一、概述
万物皆对象,异常在Java中也是一种对象。在Java中,把常见的异常情况,都抽象成了对应的异常类型,那么每种异常类型都代表了一种特定的异常情况。
当程序出现异常情况时,会创建并抛出和该异常情况对应的异常类的对象,这个异常对象中保存了一些信息,用来表示当前程序到底发生了什么异常情况。
通过异常信息,我们可以定位异常发生的位置,以及异常发生的原因。
异常分为两种,Java提供的异常类型称为原生异常,此外也可以自定义异常。
异常体系中的根类是:java.lang.Throwable,该类下面有两个子类型:java.lang.Error和java.lang.Exception。其中Error是严重的错误,程序自身不能够解决;而Exception是异常,能够通过特定的方式进行处理和纠正,平常表示的异常都是这种异常。
Exception中并没有定义方法,它的方法都是从Throwable中继承过来的,其中常用的有:
- printStackTrace():打印输出当前发送异常的详细信息(重要)。
- getMessage():返回异常对象抛出是携带的信息,一般是异常的发生原因(重要)。
- printStackTrace(PrintWriter s):可以指定字符输出流,对异常信息进行输出。
- printStackTrace(PrintStream s):可以指定字节输出流,对异常信息进行输出。
异常主要分为两种:
- 运行时异常:编译器不会报错,之后在运行时抛出。
- 编译时异常:又称检查异常,编译器会报错。
出现异常后的处理的方法有两种:
- 将异常抛出:基于throws关键字对异常进行抛出,并可以在方法中对异常使用throw进行手动抛出。
- 捕获异常并处理:基于try-catch-finally结构对异常进行捕获和处理。
二、抛出
JVM会在出现了预设的异常情况之时,自动创建异常对象,抛出、捕获并处理(打印)。
也可以对可能出现的异常在方法中使用throw手动抛出。
- 若该异常为编译时异常,则需要在方法体之前使用throws关键字将异常进行声明。
- 若该异常为运行时异常,则不需要使用throws关键字进行声明。
当方法将异常抛出后,若异常一直没有被调用方法的代码处理而继续抛出(即没有方法对此进行处理),JVM会自动捕获并通过printStackTrace()方法将异常打印出来。
public void test(String name) { // 运行时异常不用throws
if(!"tom".equals(name)) {
throw new RuntimeException("用户名和预期不符!");
}
}
public void test(String name) throws Exception {
if(!"tom".equals(name)) {
throw new Exception("用户名和预期不符!");
}
}
三、捕获和处理
若不想让方法对异常进行抛出,让JVM捕获和处理,可以使用try-catch代码块进行捕获和处理。在try块中,会对其中的代码进行捕获;在catch块中,对特定的异常进行处理。
- catch可以使用多次,对特定的异常进行处理。需先捕获范围小的异常,再捕获范围大的异常。
- 也可以使用|对异常进行捕获。
- 也可以不使用catch捕获异常,此时需要在方法体之前声明编译时异常。
在捕获并处理异常后,方法会终止。若try块中还有后续代码,则不会被执行。故引入了finally关键字,无论是否发生异常,finally块内的代码都会被执行。同时,finally块的代码执行是在return之后的,即使发生了return,finally代码依然会执行。若finally代码中存在return,会覆盖先前的return。try {}
catch(RuntimeException e) {} // 先捕获范围小的异常
catch(Exception e) {} // 再捕获范围大的异常
/***********分割线***********/
try {}
catch(ClassNotFoundException | NoSuchMethodException e) {}
当然,try-catch-finally是配合执行的,当try代码没有被执行时,finally代码也不会被执行。在try块或catch块时被打断(interrupted)或者被终止(killed),或执行了 System.exit(0)语句终止了JVM的运行,finally代码也都不会被执行。try {
return 10/0;
return 1;
} catch(Exception e) {
return 2;
} finally {
return 3; // 最后返回3
}
/***********分割线***********/
int i = 0;
try {
return 10/0;
return i++;
} catch(Exception e) {
return i++; // 最后返回0
} finally {
i++;
}
针对流等资源类型对象的异常处理(try-with-resource资源自动释放机制),参见节点流。四、自定义异常
JDK中存在的异常都是与程序编译和运行的关键错误有关的。若程序的使用者使用了程序的编写者不允许的非法指令,如年龄设置为负数、成绩大于满分,用户登录时密码不正确、用户访问某些接口时候的权限不足等,这时则需要使用自定义异常。可以定义一个类继承RuntimeException(运行时异常)或Exception(编译异常),并使用父类的构造方法,即可自定义一个异常。一般继承RuntimeException。
抛出信息如果每次都不一样,日志就会混乱。可以使用枚举对异常的抛出信息进行统一约定:public class ModifyUserInfoException extends RuntimeException { // 自定义一个用户信息修改异常,继承运行时异常类
public ModifyUserInfoExceptin() {}
public ModifyUserInfoExceptin(String message) {
super(message); // 调用父类的构造方法
}
}
// 抛异常时
throw new ModifyUserInfoException("权限错误");
public enum ExceptionType {
ACCESS_ERROR("权限错误"); // 定义一个权限错误异常
private String message;
private ExceptionType(String message) { // 构造器
this.message = message;
}
public String getMsg() {
return message;
}
}
// 抛异常时
throw new ModifyUserInfoException(ExceptionType.ACCESS_ERROR.getMsg());
五、异常预处理
编译异常在使用IDE编写程序的时候即出现,故编译异常是能够在编译前处理和避免的。
而为了避免运行时异常,则需要注意以下几点:
(一)对变量可能的值进行预见
对于基本数据类型,需要对数据的正负、是否为0进行判断,避免出现不应为负值的值为负、除数为0等情况。
对于引用类型,需要对引用类型是否为null进行判空处理,避免对象在使用方法时,对象为空的情况(空指针异常)。同时,字符串在进行比较时,一般使用”固定”.equals(变量),而不是变量.equals(“固定”),防止变量为null的情况。
(二)对数组的范围进行预见
在存入数据时,需要对数组的元素数量的当前长度进行比较,确定是否能够存入。
(三)对数据强转的类型进行预见
在进行类型强制转换时,需要对对象是否属于某个类进行判断(如instanceof),确定是否能够强转。六、断言
断言(assert)是JDK1.4中增加的关键字。可以在程序中,使用断言确认一些关键性条件必须成立(true),否则会抛出AssertionError类型的错误。
默认情况下,JVM没有开启断言功能,需要通过给JVM传参(-enableassertions 或 -ea)来开启此项功能。assert a != 0 : "参数a的值不能为0";
// a为0时出现异常:Exception in thread "main" java.lang.AssertionError: 参数a的值不能为0