深入理解Java异常

 异常时什么?就是指阻止当前方法或作用域继续执行的问题,当程序运行时出现异常时,系统就会自动生成一个Exception对象来通知程序进行相应的处理。Java异常的类型有很多种,下面我们就使用一张图来看一下Java异常的继承层次结构:

1 Throwable

Throwable 类是 Java 语言中所有错误或异常的超类(这就是一切皆可抛的东西)。它有两个子类:ErrorException

2 Error

用于指示合理的应用程序不应该试图捕获的严重问题。这种情况是很大的问题,大到你不能处理了,所以听之任之就行了,你不用管它。比如说VirtualMachineError:当 Java 虚拟机崩溃或用尽了它继续操作所需的资源时,抛出该错误。好吧,就算这个异常的存在了,那么应该何时,如何处理它呢??交给JVM吧,没有比它更专业的了。

 

3 Java异常中的Exception

上面我们有介绍,Java异常的中的Exception分为受检查异常和运行时异常(不受检查异常)。下面我们展开介绍。

Java中的受检查异常  CheckedException

CheckedException需要用try...catch...显示的捕获。       对于可恢复的条件

相信大家在写IO操作的代码的时候,一定有过这样的记忆,对File或者Stream进行操作的时候一定需要使用try-catch包起来,否则编译会失败,这是因为这些异常类型是受检查的异常类型。编译器在编译时,对于受检异常必须进行try...catch或throws处理,否则无法通过编译。常见的受检查异常包括:IO操作、ClassNotFoundException、线程操作等。

Java中的非受检查异常(运行时异常) UncheckedException

UncheckedException不需要捕获。通常UncheckedException又叫做RuntimeException。  对于程序错误(言外之意不可恢复,大错已经酿成)

RuntimeException及其子类都统称为非受检查异常,例如:NullPointExecrption(空指针异常)、NumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

4 Java的异常处理

Java中try和catch

Java处理异常的一般格式是这样的:

try {
  File file = new File("d:/a.txt");
  if(!file.exists())
    file.createNewFile();
} catch (IOException e) {
  // TODO: handle exception
}

try块中放置可能会发生异常的代码(但是我们不知道具体会发生哪种异常)。如果异常发生了,try块抛出系统自动生成的异常对象,然后异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进行catch语句执行(不会在向下查找)。如果我们的catch语句没有匹配到,那么JVM虚拟机还是会抛出异常的。

Java中的throws关键字

在Java中还提供了另一种异常处理方式即抛出异常,顾名思义,也就是说一旦发生异常,我把这个异常抛出去,让调用者去进行处理,自己不进行具体的处理,此时需要用到throw和throws关键字。 

public class Main {
    public static void main(String[] args) {
        try {
            createFile();
        } catch (Exception e) {
            // TODO: handle exception
        }
    }
     
    public static void createFile() throws IOException{
        File file = new File("d:/a.txt");
        if(!file.exists())
            file.createNewFile();
    }
}

这段代码和上面一段代码的区别是,在实际的createFile方法中并没有捕获异常,而是用throws关键字声明抛出异常,即告知这个方法的调用者此方法可能会抛出IOException。那么在main方法中调用createFile方法的时候,采用try...catch块进行了异常捕获处理。

Java中的throw关键字

当然还可以采用throw关键字手动来抛出异常对象。下面看一个例子:

public class Main {
    public static void main(String[] args) {
        try {
            int[] data = new int[]{1,2,3};
            System.out.println(getDataByIndex(-1,data));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
         
    }
     
    public static int getDataByIndex(int index,int[] data) {
        if(index<0||index>=data.length)
            throw new ArrayIndexOutOfBoundsException("数组下标越界");
        return data[index];
    }
}

总结

 也就说在Java中进行异常处理的话,对于可能会发生异常的代码,可以选择三种方法来进行异常处理:

1)对代码块用try..catch进行异常捕获处理;

2)在 该代码的方法体外用throws进行抛出声明,告知此方法的调用者这段代码可能会出现这些异常,你需要谨慎处理。此时有两种情况:

 如果声明抛出的异常是非运行时异常,此方法的调用者必须显示地用try..catch块进行捕获或者继续向上层抛出异常。

 如果声明抛出的异常是运行时异常,此方法的调用者可以选择地进行异常捕获处理。

3)在代码块用throw手动抛出一个异常对象,此时也有两种情况,跟2)中的类似:

  如果抛出的异常对象是非运行时异常,此方法的调用者必须显示地用try..catch块进行捕获或者继续向上层抛出异常。

  如果抛出的异常对象是运行时异常,此方法的调用者可以选择地进行异常捕获处理。

  (如果最终将异常抛给main方法,则相当于交给jvm自动处理,此时jvm会简单地打印异常信息)

5 try,catch,finally,throws,throw五个关键字   

 1.try,catch,finally

try关键字用来包围可能会出现异常的逻辑代码,它单独无法使用,必须配合catch或者finally使用。Java编译器允许的组合使用形式只有以下三种形式:

&emsp;&emsp;try...catch...;       try....finally......;    try....catch...finally...

当然catch块可以有多个,注意try块只能有一个,finally块是可选的(但是最多只能有一个finally块)。

当然如果没有发生异常,则catch块不会执行。但是finally块无论在什么情况下都是会执行的(这点要非常注意,因此部分情况下,都会将释放资源的操作放在finally块中进行)。

  在有多个catch块的时候,是按照catch块的先后顺序进行匹配的,一旦异常类型被一个catch块匹配,则不会与后面的catch块进行匹配。


在使用try..catch..finally块的时候,注意千万不要在finally块中使用return,因为finally中的return会覆盖已有的返回值。下面看一个例子:。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
 
 
public class Main {
    public static void main(String[] args) {
        String str = new Main().openFile();
        System.out.println(str);
         
    }
     
    public String openFile() {
        try {
            FileInputStream inputStream = new FileInputStream("d:/a.txt");
            int ch = inputStream.read();
            System.out.println("aaa");
            return "step1";
        } catch (FileNotFoundException e) {
            System.out.println("file not found");
            return "step2";
        }catch (IOException e) {
            System.out.println("io exception");
            return "step3";
        }finally{
            System.out.println("finally block");
            //return "finally";
        }
    }
}

 

最后打印出的是"finally",返回值被重新覆盖了。

  因此如果方法有返回值,切忌不要再finally中使用return,这样会使得程序结构变得混乱。

 

throws和thow关键字

       1)throws出现在方法的声明中,表示该方法可能会抛出的异常,然后交给上层调用它的方法程序处理,允许throws后面跟着多个异常类型;

  2)一般会用于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。throw只会出现在方法体中,当方法在执行过程中遇到异常情况时,将异常信息封装为异常对象,然后throw出去。throw关键字的一个非常重要的作用就是 异常类型的转换(会在后面阐述道)。

  throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由方法去处理异常,真正的处理异常由此方法的上层调用处理。   

6 在类继承的时候,方法覆盖时如何进行异常抛出声明

本小节讨论子类重写父类方法的时候,如何确定异常抛出声明的类型。下面是三点原则:

  1)父类的方法没有声明异常,子类在重写该方法的时候不能声明异常;

  2)如果父类的方法声明一个异常exception1,则子类在重写该方法的时候声明的异常不能是exception1的父类;

  3)如果父类的方法声明的异常类型只有非运行时异常(运行时异常),则子类在重写该方法的时候声明的异常也只能有非运行时异常(运行时异常),不能含有运行时异常(非运行时异常)。

7 常见面试题

1. Error 和 Exception 区别是什么?

Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;

Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。

2. 运行时异常和一般异常区别是什么?

编译器不会对运行时异常进行检测,没有 try-catch,方法签名中也没有 throws 关键字声明,编译依然可以通过。如果出现了 RuntimeException, 那一定是程序员的错误。

一般一场如果没有 try-catch,且方法签名中也没有用 throws 关键字声明可能抛出的异常,则编译无法通过。这类异常通常为应用环境中的错误,即外部错误,非应用程序本身错误,如文件找不到等。

3.NoClassDefFoundError 和 ClassNotFoundException 区别?

NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。

引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;

ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

 

4. JVM 是如何处理异常的?

在一个方法中如果发生异常,这个方***创建一个一场对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。

JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

5. throw 和 throws 的区别是什么?

throw 关键字用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
throws 关键字用在方法签名处,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

6. 常见的 RuntimeException 有哪些?

  • ClassCastException(类转换异常)
  • IndexOutOfBoundsException(数组越界)
  • NullPointerException(空指针)
  • ArrayStoreException(数据存储异常,操作数组时类型不一致)
  • 还有IO操作的BufferOverflowException异常

7 常见的error

异常处理的原则

我们在日常处理异常的代码中,应该遵循三个原则

  • 不要捕获类似 Exception 之类的异常,而应该捕获类似特定的异常,比如 InterruptedException,方便排查问题,而且也能够让其他人接手你的代码时,会减少骂你的次数。

  • 不要生吞异常。这是异常处理中要特别注重的事情。如果我们不把异常抛出来,或者也没有输出到 Logger 日志中,程序可能会在后面以不可控的方式结束。

  • 不要在函数式编程中使用 checkedException

 

参考链接

https://juejin.im/post/5ae66791f265da0b92655c5d#heading-5

https://juejin.im/post/5b6d61e55188251b38129f9a