第9章_异常

gong_yz大约 10 分钟JAVAJAVASE

1、Java的异常体系

Error和Exception

  • Exception和 Error, 二者都是 Java异常处理的重要子类, 各自都包含大量子类。均继承自Throwable类。
  • Error表⽰系统级的错误, 是java运⾏环境内部错误或者硬件问题, 不能指望程序来处理这样的问题, 除了退出运⾏外别⽆选择, 它是Java虚拟机抛出的。
  • Exception 表⽰程序需要捕捉、 需要处理的常, 是由与程序设计的不完善⽽出现的问题, 程序必须处理的问题。

2、异常类型

2.1 受检异常

对于受检异常来说, 如果⼀个⽅法在声明的过程中证明了其要有受检异常抛出:

public void test() throw new Exception{ }

那么,当我们在程序中调用他的时候, ⼀定要对该异常进⾏处理( 捕获或者向上抛出) , 否则是⽆法编译通过的。 这是⼀种强制规范。

这种异常在IO操作中⽐较多。 比如FileNotFoundException , 当我们使⽤IO流处理⼀个⽂件的时候, 有⼀种特殊情况, 就是⽂件不存在, 所以, 在⽂件处理的接⼜定义时他会显⽰抛出FileNotFoundException, ⽬的就是告诉这个⽅法的调⽤者,我这个⽅法不保证⼀定可以成功, 是有可能找不到对应的⽂件 的, 你要明确的对这种情况做特殊处理哦。

所以说, 当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。

2.2 非受检异常

对于⾮受检异常来说, ⼀般是运⾏时异常, 继承自 RuntimeException。 在编写代码的时候, 不需要显⽰的捕获,但是如果不捕获, 在运⾏期如果发⽣异常就会中断程序的执⾏。

这种异常⼀般可以理解为是代码原因导致的。 ⽐如发⽣空指针、 数组越界等。 所以, 只要代码写的没问题, 这些异常都是可以避免的。 也就不需要我们显⽰的进⾏处理。

试想⼀下, 如果你要对所有可能发⽣空指针的地⽅做异常处理的话, 那相当于你的所有代码都需要做这件事。


3、和异常有关的关键字

throws、 throw、 try、 catch、 finally。

3.1 声明异常

在Java中,想要声明某一个方法可能抛出的异常信息时,需要用到throws关键字。通过使用throws,表示异常被声明,但是并不处理。

//使用throws声明异常
public void method() throws Exception{
    //方法体
}

以上方法通过throws声明了method可能抛出Exception异常,需要这个方法的调用者来处理这个异常。

3.2 抛出异常

throw---明确抛出一个异常

在方法体中,想要明确抛出一个异常时,可以使用throw:

//使用throws声明异常
public void method() throws Exception{
    //使用throw声明异常
    throw new Exception();

}

在异常被抛出后,需要被处理,Java中异常的处理方式主要有两种:

  • 自己处理
  • 向上抛,交给调用者处理

对于继续向上抛的这种处理方式,一般根据异常的类型有不同的方式,如果是受检异常,则需要明确的再次声明异常,而非受检异常则不需要。例如:

public void clear() throws Exception{
    method()
}

public void method throws Exception{
    throw new Exception();

}

caller方法调用了method方法,因为method方法声明了一个受检异常Exception,那么对于调用者来说,如果无法处理,就需要继续向上抛。这里需要在caller方法中同样使用throws声明异常。

3.3 捕获异常

在Java中,异常的捕获需要用到try、catch、finally等关键字。

  • try:用来指定一块预防所有异常的程序
  • catch:紧跟在try块后面,用来指定想要捕获的异常的类型
  • finally:为确保一段代码不管发生什么异常状况都要被执行

在如下一段异常的捕获代码中,try是必须的,catch和finally至少有一个:

try{
	//代码块
}
catch(异常类型 异常对象){
	//异常处理
}
finally{
	//一定会执行的代码
}
try{
	//代码块
}
catch(异常类型 异常对象){
	//异常处理
}
try{
	//代码块
}
finally{
	//一定会执行的代码
}

其中,try和finally只能有一个,但catch可以有多个,即在一次异常捕获过程中,可以同时对多个异常进行捕获。例如:

try{
	//代码块
}
catch(Exception1 e1){
	//异常处理
}
}
catch(Exception2 e2){
//异常处理
}
}
catch(Exception3 e3){
//异常处理
}

4、异常链

“异常链”是Java中⾮常流⾏的异常处理概念, 是指在进⾏⼀个异常处理时抛出了另外⼀个异常, 由此产⽣了⼀个异常链条。

该技术⼤多⽤于将“ 受检查异常” ( checked exception) 封装成为“⾮受检查异常”( unchecked exception)或者RuntimeException。

顺便说⼀下, 如果因为因为异常你决定抛出⼀个新的异常, 你⼀定要包含原有的异常, 这样, 处理程序才可以通过getCause()和initCause()⽅法来访问异常最终的根源。

以下是Throwable中支持异常链的方法和构造函数:

Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

以下示例显示如何使用异常链

try {
    // 代码块 
} catch (Exception1 e1) {
    throw new Exception2(e1);
}

这样我们在处理异常Exception2时,就能够知道这个异常是由Exception1 引起的,更加方便我们排查问题。


5、异常处理的最佳实践

1、不要使用异常来控制业务逻辑

在开发具体业务时,很多人会使用异常来控制业务逻辑,例如:

try{
    execute();
}catch(Exception e){
    execute1();
}catch(Exception1 e1){
    execute2(); 
}

代码中的分支逻辑应该使用if-else来控制,而不是依赖异常。使用异常来控制代码逻辑不容易理解,难于维护。

2、如果处理不了,请不要捕获

很多人在开发中,遇到try语句块,后面一定会跟一个catch块,这是不对的。我们在开发中,应该只捕获那些能处理的异常,如果处理不了,就不要捕获它,继续向上抛,谁能解决谁来捕获。

像下面这段代码对于异常的处理毫无意义:

try{
 
}catch(Exception e){
    throw e;
}

甚至还有如下异常处理方式:

try{
 
}catch(Exception e){
    //这是一个异常,忽略他
}

什么也没做,还写了一行无用注释。

3、在catch语句中不要使用printStackTrace()

需要注意的是,e.printStackTrace()并不是处理异常,很多人认为这是在打印异常信息,也是在处理异常。

但是,e.printStackTrace()表示把异常信息输出到控制台,而不是日志中。e.printStackTrace()只能在调试阶段使用,程序在线上运行时,错误的信息要通过日志输出。

4、二次抛出异常时,要带上异常链

在抛出新的异常时,一定要把被捕获的那个异常的异常信息也带上,避免丢失异常的堆栈。例如:

try{
}catch(Exception e){
    //错误用法
    throw new MyException();
    //正确用法
    throw new MyException(e);
}

5、在需要的地方声明特定的受检异常

受检异常最大的特点是要求调用者必须明确地处理这个异常,这其实是一种强制性的约束。所以,当代码中有一些特殊情况需要让调用者必须关注时,要使用受检异常,起到提醒的作用。

6、异常捕获的顺序需要特殊注意

try{
}
catch(Exception e){
}
catch(MyException e){
}
catch(IllegalArgumentException e){
}

以上处理方式最大的问题就是异常的捕获顺序不合理,以上形式的捕获异常,后面的MyException和IllegalArgumentException 永远不会被捕获,异常一旦发生就会被Exception直接捕获了。

所以,在捕获异常时,要把范围较小的异常放到前面,对于RuntimeExceotion、Exception和Throwable的捕获一定要放到最后。

7、可以直接捕获Exception,但是要注意场景

我们需要在RPC接口中对Exception进行捕获,以避免异常交给外部系统。

8、可以直接捕获Throwable,但是要注意场景

当我们提供RPC服务时,一旦服务调用过程中发生了Error,如NoSuchMethodError,我们没有捕获,那么这个错误就会一直往上抛,最终会被RPC框架捕获。

RPC框架捕获错误之后,可能会把错误日志打印到它自己的日志文件中,而不是我们应用的业务日志中。 通常RPC框架自己的日志会有很多各种超时等异常,我们很少对其进行错误监控,这就可能导致错误发生了,但我们无法察觉。

9、不要在finally中抛出异常

示例代码如下:

try{
    execute();
}finally{
    throw new Exception();
}

当execute方法抛出异常之后,我们在finally中再次抛出一个异常,这就导致execute方法抛出的那个异常信息完全丢失了。丢失了异常链,会给后期的问题排查带来很大的困难。

10、在finally中释放资源

当我们像释放一些资源时,如数据库链接,文件链接等,需要在finally中进行释放,因为finally中的代码一定会执行。

11、如果不想处理异常,则使用finally块而不是catch块

当我们不想处理一个异常,又想在异常发生后做一些事情的时候,不要写catch块,而是使用finally块。

12、善于使用自定义异常

我们可以自定义一些业务异常,这些异常可以有一定的继承关系,方便我们快速的识别异常原因,以及快速恢复。比如OrderCanceledException,LoginFailedException等。我们通过这些异常名字就知道具体发生了什么。

13、try块中的代码要尽可能的少


6、自定义异常

参考:https://mp.weixin.qq.com/s/2o85Oel6GMp1QUENE-lEywopen in new window


7、try-with-resources

在Java中,对于文件操作的I/O流、数据库连接等开销非常大的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能导致内存泄漏等问题。

关闭资源的常用方式就是在finally块中进行处理,即调用close方法。比如,我们经常会写这样的代码:

image-20230303144403044
image-20230303144403044
image-20230303144418681
image-20230303144418681
image-20230303144428067
image-20230303144428067

8、finally是在什么时候执行的

我们知道,finally的代码一定会被执行,那么,它是在什么时间点执行的呢?比如以下代码,test()的返回结果应该是多少?

public int test(){
    int a = 1;
    try {
       return a+1;
    }finally {
       a= 2;
    }
}

以上代码的输出结果为2。

如果在finally中也加一个return:

public int get() {
    int a=2;
    try {
       return a+1;
    } finally {
       a = 2;
       return a;
    }
}

以上代码的输出结果为2。

虽然finally代码会执行,但是在return后面的表达式运算后执行的,所以函数返回值是在finally执行前就确定了。

也就是说,finally中的代码会在a+1计算之后、return执行返回操作之前执行。

如果在finally中也有return语句,那么方法就会提前返回,而返回的结果就是finally中retun的值。

不知道读者会不会有这样的问题:如果在trv/catch块中,JVM突然中断了(比如使用了System.exit(0)),那么finally中的代码还会执行吗? 比如以下代码:

public void print(){
    try{
        System.out.print1n("try");
        System.exit(0);
    }finally {
       System.out.println("finally");
    }
}

输出结果为try。

finally的执行需要两个前提条件:

  • 对应的try语句块被执行;
  • 程序正常运行。当使用System.exit(0)中断执行时,finally就不会再执行了。