君志所向,一往无前,愈挫愈勇,再接再厉。
内容
1.异常概述
异常指不期而至的各种状况,如:文件找不到、网络连接失败、除0操作、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。为了应对运行期间可能出现的错误,提高程序的的稳健性,Java中定义了强大的异常处理机制,使用异常处理机制可以降低错误处理代码的复杂度:
- 异常处理程序中集中处理错误,节省代码,不必每次都去处理。
- 执行过程和异常处理相分离,更易阅读、编写、调试。
2.Java的异常处理机制
Java把异常当作对象来处理,并通过抛出异常和捕获异常完成对各种异常的处理。
抛出异常:抛出异常是指:当前环境下无法获得必要的信息来解决问题,就从当前环境中跳出,并把问题提交给上一级环境。抛出异常后,首先,将使用new在堆上创建一个异常对象,然后当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。
捕获异常:在方法抛出异常之后,异常处理器通过捕获异常的类型,来执行相应异常类型的异常处理程序或者异常处理器,让程序重新恢复到稳定状态。
3.Java异常体系和分类
上面这张图展示了Java异常的继承结构关系,可以发现所有的异常都是Throwable的子类。
Java语言提供了三种可抛结构:受检异常、运行时异常、错误,其中点运行时异常和错误统称非受检异常。
- 受检异常(又称非运行时异常):编译器要求必须处理的异常,要么处理异常,要么在 异常说明中表明此方法将产生异常,否则编译不通过。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常,如:IOException、SQLException、ClassNotFoundException(异常说明稍后会在throws中详细讲到)
- 运行时异常:编译器不要求强制处置的异常。包括RuntimeException及其子类,他们会被JVM自动抛出、自动捕获,一般由程序员粗心大意造成,是编程错误,这些错误在编码过程中是可以避免的,如:NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组下标越界异常)。.
- 错误(Error):指程序无法处理的错误,编译器不要求强制处置。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM 出现的问题,如当虚拟机内存溢出时出现:OutOfMemoryError,这些异常发生时,JVM一般会选择线程终止。
4.throw和throws:异常抛出和异常声明
throw:
throw总是出现在方法体中,用来抛出一个Throwable类型的异常对象。例如:
if(obj == null){
throw new NullPointerException();
}
使用throw应该注意:
- throw必须抛出Throwable的子类对象。
- 程序执行完throw语句会从当前作用域退出,其后的语句不再执行。
- 不要在构造函数中抛出异常,这可能会导致对象创建失败。
throws:
throws用于方法的异常说明,它属于方法声明的一部分,紧跟形参列表之后,列举了一个方法可能引发的所有异常类型。例如:
void f() throws IllegalAccessException, IOException{
//....
}
当一个方法中通过throw关键字抛出了多个异常,而又不想或者没有能力去处理这些异常时,可以通过throws将这些异常抛给调用者。
使用throws应该注意:
- 如果是非受检异常,即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,当发生非受检异常时,JVM自动抛出该异常。
- 如果一个方法可能出现受检异常,要么用try-catch语句捕获处理,要么用throws子句声明将它抛出。
- 如果方法中存在多个未被处理的受检异常,那么这些受检异常必须在throws后全部声明。
- 子类重写父类方法时,被重写的方法可以抛出任意非受检异常,不能抛出新的受检异常,子类抛出的受检异常必须是被重写方法的本类或子类。
class TestThrows {
static void throw1() { //RuntimeException是非受检异常,可以在throws后省略
throw new RuntimeException();
}
//IllegalAccessException和IOException 均为受检异常,且未被处理
static void throw2() throws IllegalAccessException, IOException {
boolean flag = new Random().nextBoolean();
if (flag) {
throw new IllegalAccessException();
} else {
throw new IOException();
}
}
}
class Fu {
void f() throws IOException {
}
}
class Zi extends Fu {
//子类重写的方法可以抛出任意非受检异常如:RuntimeException、Error
//子类重写的方法抛出的受检异常必须是父类受检异常的本类或子类,
//FileNotFoundException为IOException的子类
@Override
void f() throws FileNotFoundException, Error{
}
}
Tips:
- 可以声明方法抛出一个异常而实际不抛异常,编译器会强制用户像真的抛出异常来使用这个方法,这样以后就可以抛出这种异常而不用修改已有的代码。
- throw和throws相差甚远,几乎没什么关系。throw只出现在方法体中,用于抛出异常;throws出现在方法声明上,用于声明方法可能出现的异常。
5.try-catch-finally异常处理
5.1 语法形式
在Java中,异常通过try-catch-finally语句捕获处理。其一般语法形式为:
try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
} catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}finally{
// 无论是否发生异常,都会执行finally中的语句
}
try块:try包含一块可能发生异常的代码,称为监控区域,用于捕获区域内出现的异常。
catch块:用于匹配和处理捕获到的异常。
finally块:无论是否发生异常,都会执行finally中的语句,通常用于释放资源,如断开网络连接、关闭文件等。
5.2 规则
- try块后可接多个catch语句块,finally为可选语句,可以与catch语句块同时存在,如果没有catch语句块,则try块后必须跟一个finally语句块。
- 必须遵循块顺序:try --> catch --> finally 。
- 若try捕获的异常类型与catch声明的异常类型一致,或try捕获的异常类型是catch声明的异常类型的子类,则匹配成功,然后执行匹配成功的catch块。
- 多个catch块按书写顺序匹配,catch中子类异常类型必须在父类异常类型之前,否则编译不通过。
- 一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束,其他的catch子句不再有匹配和捕获异常类型的机会。
- finally语句不是一定会执行,例如:JVM过早终止(调用了System.exit(0);)、finally语句块有未被处理的异常、突然断电等都可能导致finally语句没有被执行。
5.3 执行顺序
- 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句。
- 当try捕获到异常,在try语句块中当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。
- 当try块或catch块中有return语句、或者catch块中有throw语句时,会在程序返回或抛出异常之前执行finally语句块。
Tip:
如果在try块抛出的是受检异常,那么一定会被catch某块匹配成功,try块抛出异常都是非受检异常,如果catch匹配失败将会把异常抛给方法调用者。
5.4 不要在finally块中做与资源释放无关的操作
finally语句块设计的初衷是用来释放资源的,不管是否发生异常,比如断开网络连接,关闭文件等,在finally块中其他操作可能使得程序出现奇怪的问题。下面列举了两个情况:
(1)finally中修改数据
public class FinallyUpdateTest{
static Dog f1() {
Dog dog = new Dog("旺财", 3);
try {
throw new IllegalAccessException();
} catch (Exception e) {
System.out.println("f1:catch");
return dog;
} finally {
System.out.println("f1:finally");
dog.setName("阿旺");
}
}
static int f2() {
int num = 10;
try {
throw new IllegalAccessException();
} catch (Exception e) {
System.out.println("f2:catch");
return num;
} finally {
System.out.println("f2:finally");
num = 20;
}
}
public static void main(String[] args) {
System.out.println(f1());
System.out.println(f2());
}
}
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
输出结果:
f1:catch
f1:finally
Dog{name='阿旺', age=3}
f2:catch
f2:finally
10
解析:
在执行finally语句块的内容之前,return的值会先被压入栈中,执行完finally语句块内容后,才会将return的值从栈中取出,最后返回。因此对于基本数据类型和String类型,finally的修改操作不会影响到原来的值,但对于引用类型来说,finally修改的是该引用所指向的对象的值。
(2)finally中的return
public class FinallyReturnTest {
static int f1() {
try {
int b = 2 / 0;
return 0;
} catch (Exception e) {
System.out.println("f1:catch");
return 1;
} finally {
System.out.println("f1:finally");
return 2;
}
}
static void f2() throws Exception {
try {
int b = 2 / 0;
} catch (Exception e) {
System.out.println("f2:catch");
throw e;
} finally {
System.out.println("f2:finally");
return;
}
}
public static void main(String[] args) {
System.out.println("f1: return " + f1());
try {
f2();
} catch (Exception e) {
System.out.println("f2()抛出异常");
}
}
}
输出:
f1:catch
f1:finally
f1: return 2
f2:catch
f2:finally
从输出结果可以看出来:
- finally中的return让方法提前返回,屏蔽了原来的返回值,同时也导致catch 抛出的异常丢失。
6.栈轨迹
printStackTrace()方法可以打印一个由栈轨迹中的元素构成的数组,数组中的每一个元素都是一个栈帧。数组的第一个元素保存的是栈顶元素,最后一个元素保存的栈底元素。
public class StackTraceTest {
static void f1() throws Exception{
f2();
}
static void f2() throws Exception{
f3();
}
static void f3() throws Exception{
throw new Exception();
}
public static void main(String[] args) {
try {
f1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
java.lang.Exception
at StackTraceTest.f3(StackTraceTest.java:14)
at StackTraceTest.f2(StackTraceTest.java:10)
at StackTraceTest.f1(StackTraceTest.java:6)
at StackTraceTest.main(StackTraceTest.java:19)
f3 --> f2 --> f1 --> main
7.异常链
有时候我们会捕获一个异常后在抛出另一个异常,并希望保留原始信息,称为异常链。JDK1.4后Throwable中所有子类构造函数都可以传递一个Throwable子类作为因由(Cause)参数来保存原始的异常信息。这样就算抛出了新的异常,也能通过该异常追踪到异常最初发生的位置。
public class ExceptionLink {
static void f1() throws Exception{
try {
f2();
} catch (Exception e) {
throw new Exception(e);
}
}
static void f2() throws Exception{
try {
f3();
} catch (Exception e) {
throw new Exception(e);
}
}
static void f3() throws Exception{
throw new Exception();
}
public static void main(String[] args) {
try {
f1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
java.lang.Exception: java.lang.Exception: java.lang.Exception
at ExceptionLink.f1(ExceptionLink.java:9)
at ExceptionLink.main(ExceptionLink.java:27)
Caused by: java.lang.Exception: java.lang.Exception
at ExceptionLink.f2(ExceptionLink.java:17)
at ExceptionLink.f1(ExceptionLink.java:7)
... 1 more
Caused by: java.lang.Exception
at ExceptionLink.f3(ExceptionLink.java:22)
at ExceptionLink.f2(ExceptionLink.java:15)
... 2 more
从中可以清楚的看到f1调用失败的因由f2,以及f2调用失败的因由f3。
8.自定义异常类
自定义异常类必须从已有的异常类继承,因此实现一个自定义异常类就非常容易了。
public class FormatException extends Exception
{
public FormatException(String message) {
super(message);
}
}