前言
在日常的开发以及平时的学习练习中,异常相信对于大家来讲并不陌生,但是对于异常的具体使用、底层实现以及分类等等可能并不是很了解。今天我就抽出了一点时间系统的整理了异常的各个知识点,希望能够帮助到大家对于Java 异常的理解与学习。
正文
一、异常的定义
异常:是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。我自己的的理解是异常通常是一种人为规定的不正常的现象,通常是写的程序产生了一些不符合规范的事情,而且异常一旦产品,程序直接挂掉。
二、异常的分类
通过我自己整理的这一张继承关系结构图,我们可以很清晰的看到异常这个庞大体系的继承关系。
其中我们可以看到 Exception类 下面又细分了两个分支(子类),分别是运行时异常(也称非检查异常[ Unchecked Exception ])和非运行时异常(也称检查异常[ Checked Exception ]),需要注意的是 Error 也是属于非检查异常。
1、运行时异常
运行时异常也称非检查异常,指的是 Error 和 RuntimeException 这两种。既然是叫运行时异常,那肯定是在程序运行期间才可能被检查出的异常,那自然编译器在编译的时候是不会检查此类异常的,并且不要求处理该类异常,所以此类异常也叫非检查异常(或者不检查异常),这一类异常一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
下图我整理了一些常见的运行时异常,如下表格所示:
2、非运行时异常
非运行时异常指的是除开 Error 和 RuntimeException 极其子类之外的异常。既然叫做非运行时异常,顾名思义,就是不是在程序运行期间检查的异常,既然不是在程序运行期间检查,那就是编译器在编译期间就会检查此类异常,所以此类异常也叫检查异常(或者叫编译时异常)。此类异常在程序运行过程中极有可能产生问题,如果程序中出现此类异常(比如说 IOException ),必须对该异常进行处理,要么使用 try-catch 捕获,要么使用 throws语句抛出,否则编译不通过。
下图我整理了一些常见的非运行时异常,如下表格所示:
三、异常的处理
既然异常产生了,就需要我们去处理。处理异常不是让异常消失了,处理异常指的是处理掉异常之后,后续的代码不会因为此异常而终止执行,代码中的异常处理其实是对非检查异常也就是运行时异常的处理。
异常的处理有两种方式:捕获异常或者抛出异常这两种方式。
1、捕获异常,通过try…catch…finally(其中finally可有可无)语句块来处理
其中 try/catch 代码块放在异常可能发生的地方,把可能会发生异常的代码直接放到 try 块里面,如果在 try 块里面有异常发生,try 块代码里面后面的代码就不执行了,直接匹配 catch 里面的异常,如果没有异常发生, 就直接执行完try里面的代码,catch 里面的代码不执行。
try…catch 语法结构如下:
try{//程序代码
}catch (Exception e){}
其中 try 不能单独出现,后面必须添加 catch 或者 finally ,意思就是要么 try…catch,要么 try…finally,如下:
//写法1
try{//程序代码
}catch (Exception e){}
//写法2
try{//程序代码
}finally {}//写法3
try{//程序代码
}catch (Exception e){} finally {}
其中,catch 可以有多个存在
//程序代码
}catch (ArrayIndexOutOfBoundsException e1){//程序代码
}catch (IndexOutOfBoundsException e2){//程序代码
}catch (RuntimeException e3){//程序代码
}
其中 catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。如果catch 有多个,捕获的异常需要从小到大捕获。
关于try 、catch 、finally 关键字的执行顺序
我们先来看看下面代码的执行:
public static void main(String[] args) {System.out.println("try开始");String str = null;int length = str.length();System.out.println("try完毕");
}
面代码的执行结果是:
try开始
Exception in thread “main” java.lang.NullPointerException
at com.example.javabasic.javabasic.ExceptionAndError.ExceptionTest.main(ExceptionTest.java:12)
再来看看下面代码的执行:
public static void main(String[] args) {System.out.println("程序最早开始执行");try{System.out.println("try开始");String str = null;int length = str.length();System.out.println("try完毕");} catch (ArrayIndexOutOfBoundsException e1){System.out.println("数组下标越界异常");}System.out.println("产生异常之后的所有程序");
}
上面代码的执行结果是:
Exception in thread “main” java.lang.NullPointerException
try开始
at com.example.javabasic.javabasic.ExceptionAndError.ExceptionTest.main(ExceptionTest.java:14)
再来看看下面的代价的执行:
System.out.println("程序最早开始执行");try{System.out.println("try开始");String str = null;int length = str.length();System.out.println("try完毕");} catch (NullPointerException e1){System.out.println("空指针异常");}System.out.println("产生异常之后的所有程序");
}
上面代码的执行结果是:
程序最早开始执行
try开始
空指针异常
产生异常之后的所有程序
通过上面三个例子,其实我们可以发现如果程序中如果有代码会出现异常,当你没有手动进行捕获的时候,当异常发生,代码直接中断,后面的代码也不会执行,当然这是显而易见的;如果你捕获了与之匹配的异常,那么在try块里面发生异常的后面的代码不会执行,位于try块之后的代码都会正常执行;如果你捕获了异常,但是发生的异常与你捕获的异常不匹配,程序也会立刻终止,后面的代码都不会执行。
try、catch、finally关键字的执行顺序的优先级是,try块中的代码会被执行,如果出现异常则执行最匹配的catch块。但是无论是否有匹配的catch块,只要有相关的finally块,那么finally块都会被执行,虽然finally块的优先级最低。
2、抛出异常,也就是在具体位置不处理,直接抛出,通过throws/throw到上层再进行处理。
当我们遇到可能发生的异常的时候,如果我们觉得自己手动处理太麻烦,自己不想处理怎么办呢?方法总比困难多,我们可以将异常抛出去,换而言之,就是丢给别人去处理,其中抛出异常有两种方式throws和throw。
第一种方式:throws声明异常
运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常,比如下面两个代码块
public static void main(String[] args) {try {testException();} catch (InterruptedException e) {e.printStackTrace();}
}private static void testException() throws InterruptedException {Thread.sleep(100);
}
或者:
public static void main(String[] args) throws InterruptedException {testException();
}
private static void testException() throws InterruptedException {Thread.sleep(100);
}
需要注意的是如果是主函数main方法调用了会抛出异常的方法,那么一般都是不建议主方法再抛出去的(如果主方法抛出去,就相当于抛给虚拟机处理了,然而虚拟机是不会处理的。),而是在主方法里进行捕获异常,再进行处理。
其中,throws抛出异常的时候可以抛出多个异常,通过逗号隔开,例如下方代码:
private static void testException() throws InterruptedException, ArrayIndexOutOfBoundsException, ArithmeticException{Thread.sleep(100);
}
第二种方式:throw抛出异常
用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结 束当前方法的执行。
四、自定义异常
Jdk 提供了很多异常,其实基本都够我们平时的日常使用了,但是,当 Jdk 提供好的这些所有异常都不能描述或者满足我的问题的时候,我们其实可以自己定义一个异常来进行描述自己问题的异常,也就是自定义异常。自定义异常都必须是 Throwable 的子类,也就是:
继承 Throwable
继承 Exception
继承 RuntimeException
那么到底继承哪一个呢?看我们的需要,
如果希望写一个检查异常类,则需要继承 Exception 类或者 Throwable 类,一般继承 Exception 类。
如果希望写一个运行时异常类(也就是非检查异常),则需要继承 RuntimeException 类。
在平时开发中以及在项目上,一般都是选择继承 RuntimeException 类的,例如下面我定义一个自定义运行时异常:
/**
* 自定义运行时异常* @Date: 2020/4/21 9:54*/
public class MyRuntimeException extends RuntimeException{public MyRuntimeException(){}//如果想对自定义异常进行描述public MyRuntimeException(String message){super(message);}
}
测试一下自定义异常:
public static void main(String[] args){testMyRuntimeException();
}
/*** 自定义异常*/
private static void testMyRuntimeException() {throw new MyRuntimeException("我的自定义异常");
}
执行结果为:
五、错误
Error 意思就是错误,Error 表示系统致命的错误,Error 是程序中无法处理的错误,表示运行应用程序中出现了严重的错误,通常是一些物理性的错误,这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。Error 类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。错误比如 JVM 虚拟机本身出现了问题、比如 StackOverFlowError 栈内存溢出、OutOfMemoryError 堆内存溢出等。在 Java 中,错误通过 Error 的子类描述。