0. 什么是异常
异常就是不正常的意思。
在程序中的概念就是:在程序执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出一个异常对象。不同的语言对异常有不同处理,java的处理方法就是进行中断处理。
异常与语法错误是两个概念,语法错误是指程序无法通过编译,无法生成对应的字节码文件。而异常是可以生成字节码文件的,只有在运行中才会发生错误。
1.异常体系
异常的根类是java.lang.Throwable。其下又有两个子类:java.lang.Error和java.lang.Exception。Java的异常通常被分为Checked异常体系和Runtime异常体系。所有RuntimeException类及其子类都被称为Runtime异常,其余的被称为Checked异常。
- Exception是指编译期异常,进行编译(写代码)java程序出现的问题。RuntimeException是指运行期异常,java程序运行过程中出现的问题。异常在程序中处理完之后,程序不用中断,会继续执行。
AnnotationTypeMismatchException | 若某个注释的类型在对该注释进行编译(或序列化)后发生了更改,而程序试图访问该注释的元素时,抛出此异常。 |
ArithmeticException | 算术错误,如除零 |
ArrayStoreException | 使用不兼容的类型为数组元素赋值 |
ArrayIndexOutOfBoundsException | 数组索引越界 |
BufferOverflowException | 缓冲区溢出异常 |
BufferUnderflowException | |
CannotRedoException | |
CannotUndoException | |
ClassCastException | 无效转换 |
CMMException | |
ConcurrentModificationException | |
DOMException | |
EmptyStackException | |
EnumConstantNotPresentException | 试图使用未定义的枚举值 |
EventException | |
IllegalArgumentException | 使用非法参数调用方法 |
IllegalMonitorStateException | 非法的监视操作,如等待未锁定的线程 |
IllegalPathStateException | |
IllegalStateException | 环境或应用程序处于不正确的状态 |
IllegalThreadStateException | 请求的操作与当前线程的状态不兼容 |
ImagingOpException | |
IncompleteAnnotationException | |
IndexOutOfBoundsException | 某些类型的索引越界 |
JMRuntimeException | |
LSException | |
MalformedParameterizedTypeException | |
MirroredTypeException | |
MirroredTypesException | |
MissingResourceException | |
NegativeArraySizeException | 使用负数长度创建数组 |
NoSuchElementException | |
NoSuchMechanismException | |
NullPointerException | 非法使用空引用 |
NumberFormatException | 字符串到数值格式的无效转换 |
ProfileDataException | |
ProviderException | |
RasterFormatException | |
RejectedExecutionException | |
SecurityException | 试图违反安全性 |
SystemException | |
StringIndexOutOfBounds | 试图在字符串边界之外进行索引 |
TypeConstraintException | |
TypeNotPresentException | 类型未找到 |
UndeclaredThrowableException | |
UnknownAnnotationValueException | |
UnknownElementException | |
UnknownTypeException | |
UnmodifiableSetException | |
UnsupportedOperationException | 遇到不支持的操作 |
WebServiceException |
- Error是指错误,在源代码阶段不解决,程序将无法运行。
2.异常的处理方式
2.0 指定位置抛出指定异常——throw关键字
使用throw关键字,可以在指定的位置抛出指定的异常。抛出指定异常,通常是根据业务需求决定的,如程序中的一些数据的执行出现与业务不符的情况,这类情况系统通常无法自己抛出,此时只能由程序员来“主动”抛出异常。
使用throw抛出异常,通常抛出的是异常的实例,并且每次只能抛出一个异常实例。
throw使用格式为:throw XXXException("异常产生原因");
使用throw有以下注意事项:
- throw关键字必须写在方法内部。
- throw关键字后边new的对象必须是Exception或者是Exception的子类对象
- throw关键字抛出指定的异常独享,我们就必须处理这个异常对象。如果throw关键字后面创建的是RuntimeException或者是其子类对象,可以不处理,默认交给JVM处理(打印异常对象,中断程序);如果throw后面创建的是编译异常(在写代码阶段就已经报错),则必须处理这个异常,要么向上跑(throws),要么try...catch...
if (obj == null) {
throw new NullPointerException("传递的参数为空");// 这个异常就是运行期异常,默认不用处理,交给JVM处理
}
if (index<0 || index>obj.length - 1)
{
throw new ArrayIndexOutOfBoundsException("数组越界了");// 这也是运行期异常
}
throw关键字可以用来对Objects进行非空判断。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
上述的方法是Objects对象的一个静态方法,用来判断对象是否为空,具体如下使用:
Objects.requireNonNull(obj);
上述方法还有一个重载方法,就是第二个参数可以是一个字符串参数:
Objects.requireNonNull(obj,"抛出空指针");
在方法后throws异常对象,如果有多个,只要throws出共同的父类就可以了。
2.1 想处理,使用try...catch
try...catch的方法就是捕获异常。
try{
可能产生异常的代码
}catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常的处理逻辑,产生异常对象之后,怎么处理异常对象
一般在工作中,会把异常的信息记录到一个日志中
}
...
catch(异常类名 变量名){
}
注意点:
- try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象。
- 如果try中产生了异常,那么就会执行catch中的异常处理逻辑,try中发生异常的语句后所有的语句都不再执行。执行完毕catch中的处理逻辑,继续执行try..catch之后的代码。如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的方法后, 继续执行try..catch之后的代码。
由于异常会引起程序跳转,即要么执行try中的语句,要么执行catch中的语句。如果有一些语句,无论是否发生异常,都需要执行。这就需要将代码写在finally中。比如打开一些资源,无论是否发生异常,最终都要把资源关闭。
try{
可能产生异常的代码
}
catch(异常类名 变量名){
... ...
}
catch(异常类名 变量名){
... ...
}
finally{
... ...
}
finally注意事项:
- finally不能单独使用,必须喝try一起使用。
- finally一般用于资源释放(资源回收),无论程序是否出现异常,最终都要释放资源。
2.2 不想处理,就将异常抛出去(throws),给别人处理
当方法内部抛出异常对象的时候,那么我们必须处理这个异常对象,可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),最终交给JVM进行中断处理。
public static void readFile(String fileName) throws FileNotFoundException{
throw new FileNotFoundException("文件不存在");// 这个异常是编译异常,抛出编译异常,就必须处理这个异常,要么throws,要么try...catch...
}
使用注意事项:
- throws关键字必须写在方法声明处。
- throws关键字后边声明的异常必须是Exception或是Exception的子类。
- 方法内部如果抛出多个异常对象,那么throws后边必须也声明多个异常,如果抛出的多个异常对象有父子类关系,那么直接声明父类异常即可。
- 调用了一个声明抛出异常的方法,我们就必须处理声明的异常,要么继续使用throws声明抛出,交给方法调用者,最终交给JVM。要么使用try...catch自己处理异常。
3.异常产生过程解析
检测出程序出现异常后,JVM会生成一个异常对象,这个异常对象包含了异常产生的内容、原因和位置。如数组越界异常就会创建:new ArrayIndexOutOfBoundsException("3");//3是越界的数组下标。
如果在异常处理过程中,没有发现异常处理逻辑,那么JVM就会将异常对象抛出给方法的调用者,如果调用者也没有处理这个异常,就会继续向上传递,直至传递给main方法,main方法没有处理的话,就会传递给JVM。JVM收到main方法传来的异常后,把异常对象(内容、原因、位置)打印在控制台,并且终端当前正在执行的java程序。
在异常对象中,有一个printStackTrace()方法,用于打印异常跟踪栈的信息。什么是异常跟踪栈?在程序运行时,经常会出现一系列方法调用,并形成“方法调用栈”。那么,只要异常没有使用try..catch..捕获,那么异常向上传递的过程则是通过方法调用栈逐层向上传播。比如main方法调用firstMethod,firstMethod调用secondMethod,secondMethod调用thirdMethod,在thirdMethod中出现异常,则会顺着方法调用栈从栈顶的thirdMethod开始,传给secondMethod,再传给firstMethod,再传给main方法,最后main方法传给JVM处理。
当然,如果是多线程的程序,线程中的异常会传播到Thread的run方法。
上图中,下方就显示了异常跟踪栈,如:
再如下列程序:
执行上述程序后,触发的异常调用栈信息如下:
4.Throwable类中还定义了一些与异常相关的对象方法
public String getMessage():获取异常的描述信息,原因。Throwable详细信息字符串
public String toString():获取异常的类型和异常描述信息(不用)
public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。JVM就是调用这个方法打印异常信息,这个方法打印出的信息最全面。
try{
... ...
}catch(IOException e){
System.out.println(e.getMessage());
System.out.println(e.toString());
System.out.println(e.printStackTrace());
}
5.自定义异常
Java支持通过自己的业务来定义异常类,比如年龄为负数、考试成绩为负数等。
自定义异常如何定义:
- 自定义一个编译期异常:自定义类并继承于java.lang.Exception
- 自定义一个运行是其的异常类:自定义类并继承于java.lang.RuntimeException
public class RegisterException extends Exception{
// 空构造方法
public RegisterException(){
super();
}
// 添加一个带异常信息的构造方法
// 查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部会调用父类带异常信息的构造方法,让父类来处理这个异常信息。
public RegisterException(String message){
super(message);
}
}
6.异常注意事项
在异常捕获时要注意使用一次捕获,多次处理的方式,即一个try,加多个catch的方式,每个catch捕获一种异常。如果多个异常有子父类关系,那么要把子类的异常对象的catch语句写在上面,否则就会报错,即先catch子类异常,再catch父类异常。如果把父类异常的catch语句写在上面,则异常变量e首先赋值的是父类异常对象,到执行到子类异常对象的catch时,就会出现父类对象赋值给子类引用的情况。而多态是子类对象赋值给父类引用。
如果父类方法中没有抛出异常,那么子类覆盖父类的方法时,只能捕获异常并处理,不能抛出异常。如果父类方法中抛出了多个异常,那么子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。上述的规则总结起来就是:子类方法声明抛出的异常类型应该是父类方法声明抛出异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
如果finally中有return语句,永远返回的是finally中的结果。
在处理业务时,有的异常不能由一个方法解决,还要方法调用者参与解决,这时候就会在catch中再次使用throw方法将异常抛出给方法调用者处理。
在日常使用异常时,还要有如下注意:
- 不要过度的使用异常。尤其是,不要使用异常处理贷代替正常的程序流程控制,因为出发异常跳转到catch中执行,会使程序的效率变慢,比如使用while(true)语句访问数组,通过ArrayIndexOutOfBoundsException来帮助跳出while循环,这种方式使得程序可读性差,执行效率也低下。
- 不要使用过于庞大的try块。try块中的代码过于庞大,会造成其中业务复杂。其后跟的多个catch块,也会使程序变得更加复杂。
- 尽量避免使用catch all语句。即不要使用catch(Throwable e){ .. .. }来一次性捕获所有异常。
- 不要忽略捕获到的异常。