一、什么是异常
异常:程序在运行过程中发生由于硬件设备问题、软件设计错误等导致的程序异常事件。(在Java等面向对象的编程语言中)异常本身是一个对象,产生异常就是产生了一个异常对象。
二、异常体系
Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。 其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非受检查异常(Unchecked Exception)和受检查异常(Checked Exception)。
java异常体系结构:
1、Error与Exception
Errorr往往是很严重的错误,是程序无法处理的异常,可以捕获但是最好不要捕获,因为捕获了也解决不了,这个不是由程序产出的,底层出现问题就让他它挂了吧,比如内存溢出(OutOfMemoryError)、线程死亡(ThreadDeath)等。这些异常发生时, Java虚拟机(JVM)一般会选择线程终止。
下图为程序模拟的内存溢出情景:
Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。 程序中应当尽可能去处理这些异常。
2、运行时异常和非运行时异常
运行时异常都是RuntimeException类及其子类异常,常见的异常有:空指针异常(NullPointerException)、下标越界异常(IndexOutOfBoundsException)、算术异常(ArithmaticException)、类型转换异常(ClassCastException) 等,这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类异常的发生。
Exception 异常主要分为两类
- 一类是 IOException(I/O 输入输出异常),其中 IOException 及其子类异常又被称作「受查异常」
- 另一类是 RuntimeException(运行时异常),RuntimeException 被称作「非受查异常」。
常见的非检查性异常
常见的检查性异常
受查异常就是指,编译器在编译期间要求必须得到处理的那些异常,你必须在编译期处理了。
作者:凯玲之恋
链接:https://www.jianshu.com/p/49d2c3975c56
来源:
著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
下面将模拟这几个运行时异常发生的场景:
数组下标越界异常
以数组下标越界异常为例分析发生的现象,在程序运行时,JVM能够检测到没有a[3]这个索引,当检测到数组下标越界异常后,JVM做了两件事:
- 创建出异常对象 new ArrayIndexOutOfBoundsException(3);
- 将异常对象抛出,抛给它的调用者。注:一旦异常被抛出,后面的程序就不再执行。
当它的调用者没有对异常进行处理时,它的调用者继续将异常向上抛出。以此类推,当main接收到这个异常也没有对数组下标越界异常进行处理时,main就将异常抛给了JVM,JVM收到异常信息后又做了两件事:
- 将异常信息以红色字体输出到控制台上;
- 停止程序的运行。
空指针异常
三、自定义异常类型
Java 的异常机制中所定义的所有异常不可能预见所有可能出现的错误,某些特定的情境下,则需要我们自定义异常类型来向上报告某些错误信息。
在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
所有异常都必须是 Throwable 的子类。
如果希望写一个检查性异常类,则需要继承 Exception 类。
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
四、异常的处理方式
1、try...catch关键字
使用 try 和 catch 关键字可以捕获异常。
try/catch 代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}
Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
2、throws/throw 关键字
如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
下面方法的声明抛出一个 RemoteException 异常:
public class className {
public void deposit(double amount) throws RemoteException {
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
3、finally关键字
finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
4、try-catch-finally 的执行顺序
try-catch-finally 执行顺序的相关问题可以说是各种面试中的「常客」了,尤其是 finally 块中带有 return 语句的情况。我们直接看几道面试题:
面试题一:
public static void main(String[] args){
int result = test1();
System.out.println(result);
}
public static int test1(){
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
}catch(Exception e){
i--;
System.out.println("catch block i = "+i);
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
return i;
}
输出结果如下:
try block, i = 2
finally block i = 10
10
这算一个相当简单的问题了,没有坑,下面我们稍微改动一下:
public static int test2(){
int i = 1;
try{
i++;
throw new Exception();
}catch(Exception e){
i--;
System.out.println("catch block i = "+i);
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
return i;
}
输出结果如下:
catch block i = 1
finally block i = 10
10
面试题二
public static void main(String[] args){
int result = test3();
System.out.println(result);
}
public static int test3(){
//try 语句块中有 return 语句时的整体执行顺序
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
return i;
}catch(Exception e){
i ++;
System.out.println("catch block i = "+i);
return i;
}finally{
i = 10;
System.out.println("finally block i = "+i);
}
}
输出结果如下:
try block, i = 2
finally block i = 10
2
是不是有点疑惑?明明我 try 语句块中有 return 语句,可为什么最终还是执行了 finally 块中的代码?
我们反编译这个类,看看这个 test3 方法编译后的字节码的实现:
0: iconst_1 //将 1 加载进操作数栈
1: istore_0 //将操作数栈 0 位置的元素存进局部变量表
2: iinc 0, 1 //将局部变量表 0 位置的元素直接加一(i=2)
5: getstatic #3 // 5-27 行执行的 println 方法
8: new #5
11: dup
12: invokespecial #6
15: ldc #7
17: invokevirtual #8
20: iload_0
21: invokevirtual #9 24: invokevirtual #10
27: invokevirtual #11
30: iload_0 //将局部变量表 0 位置的元素加载进操作栈(2)
31: istore_1 //把操作栈顶的元素存入局部变量表位置 1 处
32: bipush 10 //加载一个常量到操作栈(10)
34: istore_0 //将 10 存入局部变量表 0 处
35: getstatic #3 //35-57 行执行 finally中的println方法
38: new #5
41: dup
42: invokespecial #6
45: ldc #12
47: invokevirtual #8
50: iload_0
51: invokevirtual #9
54: invokevirtual #10
57: invokevirtual #11
60: iload_1 //将局部变量表 1 位置的元素加载进操作栈(2)
61: ireturn //将操作栈顶元素返回(2)
-------------------try + finally 结束 ------------
------------------下面是 catch + finally,类似的 ------------
62: astore_1
63: iinc 0, 1
.......
.......
从我们的分析中可以看出来,finally 代码块中的内容始终会被执行,无论程序是否出现异常的原因就是,编译器会将 finally 块中的代码复制两份并分别添加在 try 和 catch 的后面。
可能有人会所疑惑,原本我们的 i 就被存储在局部变量表 0 位置,而最后 finally 中的代码也的确将 slot 0 位置填充了数值 10,可为什么最后程序依然返回的数值 2 呢?
仔细看字节码,你会发现在 return 语句返回之前,虚拟机会将待返回的值压入操作数栈,等待返回,即使 finally 语句块对 i 进行了修改,但是待返回的值已经确实的存在于操作数栈中了,所以不会影响程序返回结果。
面试题三
public static int test4(){
//finally 语句块中有 return 语句
int i = 1;
try{
i++;
System.out.println("try block, i = "+i);
return i;
}catch(Exception e){
i++;
System.out.println("catch block i = "+i);
return i;
}finally{
i++;
System.out.println("finally block i = "+i);
return i;
}
}
运行结果:
try block, i = 2
finally block i = 3
3
其实你从它的字节码指令去看整个过程,而不要单单四记它的执行过程。
你会发现程序最终会采用 finally 代码块中的 return 语句进行返回,而直接忽略 try 语句块中的 return 指令。
最后,对于异常的使用有一个不成文的约定:尽量在某个集中的位置进行统一处理,不要到处的使用 try-catch,否则会使得代码结构混乱不堪。
参考:
https://www.cnblogs.com/williamjie/p/9103658.html
https://www.jianshu.com/p/49d2c3975c56
https://www.xuebuyuan.com/3248044.html
https://www.jb51.net/article/128611.htm