文章目录
- 什么是异常?
- 异常的体系结构
- 异常的分类
- 一个异常的完整执行流程
- 异常的处理
- 防御式编程
- 异常的声明
- 异常的捕获
- 异常声明throws
- try-catch捕获并处理
- finally
- 异常的处理流程
- 自定义异常类
什么是异常?
在Java中,将程序执行过程中出现的不正常行为称为异常。比如:
//算术异常
System.out.println(10/0);
//输出:Exception in thread "main" java.lang.ArithmeticException: / by zero
//数组越界异常
int[] arr = {1,2,3};
System.out.println(arr[10]);
//输出:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
//空指针异常
int[] array = null;
System.out.println(array.length);
//输出:Exception in thread "main" java.lang.NullPointerException
异常的体系结构
异常在Java中以类的形式出现,每一个异常类都可以创建异常对象。异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
注:
- throwable是异常体系的顶层类,其派生出两个重要子类error类和exception类;
- error类:指JVM无法解决的严重问题,如:JVM的内部错误、资源耗尽等;
- exception类:指异常产生后程序员可以通过代码进行处理,使程序继续执行的问题;
异常的分类
可以将异常分为:
- 编译时异常,也称为 受检查异常,要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错;
编译时异常是在编译阶段发生的吗?
不是。是在编写程序的时候发生 - 运行时异常,也称为 非受检查异常,在编写程序阶段程序员可以预先处理,也可以不管,都行。;
注:
- 异常分为编译时异常和运行时异常,但所有异常都是在运行阶段发生的,因为,只有运行阶段才可以new对象,异常的发生就是new异常对象。
- 编译时异常一般发生的概率比较高,需要在运行之前进行预处理;运行时异常一般发生的概率比较低,在运行之前不必进行预处理。
一个异常的完整执行流程
public static void main(String[] args) {
System.out.println(10/0);
//这里的hello world 没有执行
System.out.println("hello world");
}
流程:
- 程序执行到System.out.println(10/0),发生了ArithmeticException算术异常,底层new了一个ArithmeticException异常对象,然后抛出了
- 由于是main方法调用了10/0,所以这个ArithmeticException抛给了main方法,但main方法没有处理,又会把这个异常抛给JVM
- JVM中止程序的执行,此时,System.out.println(“hello world”)不会执行。
异常的处理
防御式编程
- LAYL:Look Before You Leap,操作之前就做充分的检查,即事前防御型
boolean ret = false;
ret = 登录游戏();
if (!ret){
处理登录游戏异常;
return;
}
ret = 开始匹配();
if (!ret){
处理开始匹配异常;
return;
}
ret = 游戏确认();
if (!ret){
处理游戏确认异常;
return;
}
ret = 载入游戏画面();
if (!ret){
处理载入游戏画面异常;
return;
}
.....
缺点:正常代码和错误代码流程放在一起,代码整体显的比较混乱。
- EAFP:It‘s Easier to Ask Forgiveness than Permission 事后获取原谅比事前获取许可更容易,也就是先操作,遇到问题再处理;即事后认错型。
try {
登陆游戏();
开始匹配();
游戏确认();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
优势:正常流程和错误流程是分离开的,程序员更关注正常流程,代码更清晰,更容易理解。
所以,我们在处理异常时的核心思想就是 EAFP,我们需要明白“异常一旦出现是必须要被处理了才能继续执行后面的代码的”,处理异常可以交给JVM处理:直接中止程序执行;交给用户自己处理:可以按照自己的意愿来进行合适的处理,再执行后面的代码;
在Java中,异常处理主要有5个关键字:throw、throws、try、catch、finally
异常的声明
在编写程序时,如果程序中出现错误,此时就需要把错误的信息告诉调用者;
在Java中,可以通过throw关键字,来声明一个指定的异常对象,将错误的信息告诉调用者。但是它不会处理异常,必须搭配throws或者try-catch来使用。
用throw声明异常后会自动抛出
由方法的调用者使用throws配合使用:上报给上一级,直到交给JVM处理程序直接中止,不会执行后面的代码;
由方法的调用者使用try-catch配合使用:用户自己处理,处理之后可以继续执行后面的代码;
public static int getElement(int[] arr,int index){
if (arr == null){
//通过throw 告诉调用者出现的异常情况信息
throw new NullPointerException("传递的数组为空");
}
if (index < 0 || index >= arr.length){
//通过throw 告诉调用者出现的异常情况信息
throw new ArrayIndexOutOfBoundsException("要访问的数组下标越界");
}
return arr[index];
}
//实现一个获取数组指定位置元素的方法
public static void main(String[] args) {
int[] arr = {1,2,3};
int ret = getElement(arr,2);
System.out.println(ret);
}
注:
- throw必须写在方法体的内部;
- 抛出的对象必须是Exception或者Exception的子类对象;
- 如果抛出编译时异常,用户必须处理,否则无法通过编译;
- 如果抛出运行时异常,用户可以不处理,交给JVM处理;
- 异常一旦抛出,其后的代码就不会执行;
异常的捕获
异常的捕获也就是异常的具体处理方式,有两种:异常声明throws和try-catch异常捕获;
异常声明throws
当方法中抛出编译时异常(使用throws抛出异常后必须有人来进行处理),用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者处理。即当前方法不处理异常,提醒方法的调用者处理异常,如果方法的调用者也不处理,则会交给JVM处理,程序直接中止运行。
- 方法抛出异常后,方法的调用者不处理,继续向上级抛出异常,直到让JVM处理:程序中止。不执行后面的代码。
public static int getElement(int[] arr,int index) throws ArrayIndexOutOfBoundsException{
return arr[index];
}
public static void main(String[] args) throws ArrayIndexOutOfBoundsException{
int[] arr = {1,2,3};
int ret = getElement(arr,3);
System.out.println("执行后面的代码");
}
//会打印出异常日志 但不会打印出 执行后面的代码
- 方法抛出异常后,方法的调用者使用try-catch处理异常。会执行后面的代码。
public static int getElement(int[] arr,int index) throws ArrayIndexOutOfBoundsException{
return arr[index];
}
public static void main(String[] args) {
int[] arr = {1,2,3};
try {
int ret = getElement(arr,3);
}catch (ArrayIndexOutOfBoundsException e){
e.printStackTrace();
}
System.out.println("执行后面的代码");
}
//会打印出异常日志 和 执行后面的代码
注:
- 只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。就像上面说的第一种解决方法
- try-catch捕捉异常后,后续代码可以执行。就像上面说的第二种解决方法。
- try语句块中的某一行出现异常,该行后面的代码不会执行
public static int getElement(int[] arr,int index) throws ArrayIndexOutOfBoundsException{
return arr[index];
}
public static void main(String[] args) {
int[] arr = {1,2,3};
try {
int ret = getElement(arr,3);
//下面的代码不会执行
System.out.println("getElement之后的语句");
System.out.println("getElement之后的语句");
}catch (ArrayIndexOutOfBoundsException e){
e.printStackTrace();
}
//这里的代码还会执行
System.out.println("执行后面的代码");
}
//会打印出异常日志 和 执行后面的代码
try-catch捕获并处理
throws并没有对异常进行真正的处理,而是将异常抛出给方法的调用者,由调用者处理;如果真正要对异常进行处理,就需要try-catch。
public static int getElement(int[] arr,int index) throws ArrayIndexOutOfBoundsException{
return arr[index];
}
public static void main(String[] args) {
int[] arr = {1,2,3};
try {
int ret = getElement(arr,3);
}catch (ArrayIndexOutOfBoundsException e){
e.printStackTrace();
}
System.out.println("执行后面的代码");
}
注:
- catch()里的 e ,表示是e引用,保存的地址是那个new出来的异常对象的地址;
- try中可能会抛出多个异常,则建议使用多个catch来一对一精确捕获;
- catch中捕获的异常类型既可以是具体的异常类型,也可以是该异常类型的父类型。
- catch中捕获多个异常时,从上到下,必须遵守异常的范围从小到大。否则先捕获到该类的父类型时,就不会继续向下检测,不会捕获到具体的异常类型了。
ArrayIndexOutOfBoundsException,NullPointerException{ return arr[index]; } public static void main(String[] args) { int[] arr = {1,2,3}; try { int ret = getElement(arr,1); }catch (ArrayIndexOutOfBoundsException e){ e.printStackTrace(); }catch (NullPointerException e){ e.printStackTrace(); }catch (Exception e){ e.printStackTrace(); } } ``` 5. catch捕获多个异常时,异常间可以用 | 分割 ```java public static int getElement(int[] arr, int index) throws ArrayIndexOutOfBoundsException,NullPointerException{ return arr[index]; } public static void main(String[] args) { int[] arr = {1,2,3}; try { int ret = getElement(arr,1); }catch (ArrayIndexOutOfBoundsException | NullPointerException | Exception){ System.out.println("出现异常"); } } ```
finally
在某些特定的代码中,我们必须要求其释放持有的资源,比如文件读取等,但是当遇到异常时,它下面的一些代码就不会被执行到,这样就会引发一些问题。因此,我们使用finall来解决这个问题。
finall子句必须和try一起出现,不能单独编写。
即使try语句块中的代码出现了异常,finall中的代码也是一定会执行的。
public static void main(String[] args) {
try {
System.out.println("try");
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("finally");
}
}
注:
- try和finally联用,没有catch;
try { System.out.println("try"); return; } finally { System.out.println("finally"); } } ``` 2. 如果try语句块中有return,那么try、finally、return的执行顺序是怎样的? 先执行try、再执行finally、最后执行return;finally执行的时机是在return以前。 3. 如何让finally失效呢? System.exit() ``` public static void main(String[] args) { try { System.out.println("try"); //退出JVM 退出之后finally中的语句就不执行了 System.exit(0); } finally { System.out.println("finally"); } } ``` 4. 一般在finally中进行一些资源清理的工作。
异常的处理流程
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
自定义异常类
Java中,虽然已经内置了丰富的异常类,但是并不能完全表示在实际开发中遇到的所有异常,此时就需要我们自己定义异常。
自定义异常有两步:
- 编写一个类,继承Exception或者RuntimeException。
- 提供两个构造方法,一个无参,一个带有String参数。
下面以登录为例,演示自定义异常类的使用。
//自定义用户名错误类
class UsernameFalseException extends Exception{
public UsernameFalseException(){
}
public UsernameFalseException(String msg){
super(msg);
System.out.println(msg);
}
}
//自定义密码错误类
class PasswordFalseException extends Exception{
public PasswordFalseException(){
}
public PasswordFalseException(String msg){
super(msg);
System.out.println(msg);
}
}
public class Test {
private static String USER_NAME = "admin";
private static String PASS_WORD = "123456";
public static void login(String username,String password) throws UsernameFalseException, PasswordFalseException {
if (!username.equals(USER_NAME)){
throw new UsernameFalseException("用户名错误");
}
if (!password.equals(PASS_WORD)){
throw new PasswordFalseException("密码错误");
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String username = scanner.nextLine();
String password = scanner.nextLine();
try {
login(username,password);
System.out.println("登录成功");
}catch (UsernameFalseException e){
e.printStackTrace();
}catch (PasswordFalseException e){
e.printStackTrace();
}
}
}