招商银行网络面试二面的面试题,了解 Exception 和 Error 之间的差异

在 Java 编程语言中,所有异常的根类是 java.lang 包下的 Throwable 类。它的两个主要子类分别是:

  • Exception:程序可以处理的异常,这些异常可以通过 catch 语句来捕获。Exception 类进一步分为 Checked Exception(受检查异常,必须处理)和 Unchecked Exception(不受检查异常,可以选择不处理)。
  • Error:此类包含程序无法处理的错误,建议不要通过 catch 语句来捕获这些错误。例如 Java 虚拟机运行错误(VirtualMachineError)、内存不足错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等。发生这些错误时,一般情况下,Java 虚拟机(JVM)会选择终止线程。

Checked Exception 和 Unchecked Exception 的区别

Checked Exception:受检查异常。在 Java 编译过程中,如果未通过 catchthrows 关键字处理受检查异常,代码无法通过编译。

例如,以下 IO 操作的代码:

图片

除了 RuntimeException 及其子类以外,其他的 Exception 类及其子类都属于受检查异常。常见的受检查异常包括:与 IO 相关的异常、ClassNotFoundExceptionSQLException 等。

Unchecked Exception:不受检查异常。在 Java 编译过程中,即使没有处理不受检查异常,代码也能正常通过编译。

RuntimeException 及其子类统称为不受检查异常,常见的有(建议牢记,因为日常开发中会频繁遇到):

  • NullPointerException(空指针异常)
  • IllegalArgumentException(参数不合法,如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式的错误,属于 IllegalArgumentException 的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException(安全异常,例如权限不足)
  • UnsupportedOperationException(不支持的操作异常,例如重复创建同一用户)

图片

Throwable 类的常用方法

  • String getMessage():返回异常发生时的简要描述。
  • String toString():返回异常发生的详细信息。
  • String getLocalizedMessage():返回异常对象的本地化信息。如果 Throwable 的子类重写了此方法,则可以生成本地化信息;若子类未重写,则返回结果与 getMessage() 相同。
  • void printStackTrace():在控制台打印 Throwable 对象中封装的异常信息。

try-catch-finally 的用法

  • try 块:用于捕获异常。可以跟零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块:用于处理 try 捕获的异常。
  • finally 块:无论是否捕获或处理异常,finally 块中的代码都会被执行。当在 trycatch 块中遇到 return 语句时,finally 块会在方法返回之前被执行。

代码示例:

try {  
    System.out.println("Try to do something");  
    throw new RuntimeException("RuntimeException");  
} catch (Exception e) {  
    System.out.println("Catch Exception -> " + e.getMessage());  
} finally {  
    System.out.println("Finally");  
}  

输出结果:

Try to do something  
Catch Exception -> RuntimeException  
Finally  

注意:切勿在 finally 块中使用 return!try 语句和 finally 块中都有 return 语句时,try 块中的 return 语句会被忽略。

finally 中的代码是否一定会执行?

并非总是如此!在某些情况下,finally 块中的代码可能不会执行。例如,在虚拟机被终止时,finally 块中的代码将不被执行。

try {  
    System.out.println("Try to do something");  
    throw new RuntimeException("RuntimeException");  
} catch (Exception e) {  
    System.out.println("Catch Exception -> " + e.getMessage());  
    System.exit(1); // 终止当前 JVM  
} finally {  
    System.out.println("Finally");  
}  

输出结果:

Try to do something  
Catch Exception -> RuntimeException  

此外,有以下两种特殊情况下,finally 块的代码也不会被执行:

  1. 程序所在的线程死亡。
  2. CPU 被关闭。

如何使用 try-with-resources 代替 try-catch-finally?

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseablejava.io.Closeable 接口的对象。
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catchfinally 块在声明的资源关闭后运行。

《Effective Java》中指出:

面对必须关闭的资源时,总是应该优先使用 try-with-resources 而不是 try-finally,这样生成的代码更简洁、更清晰,异常处理也更有效。

在 Java 中,类似 InputStreamOutputStreamScannerPrintWriter 等资源都需要调用 close() 方法手动关闭。以下是通过 try-catch-finally 语句实现的代码:

// 读取文本文件的内容  
Scanner scanner = null;  
try {  
    scanner = new Scanner(new File("D://read.txt"));  
    while (scanner.hasNext()) {  
        System.out.println(scanner.nextLine());  
    }  
} catch (FileNotFoundException e) {  
    e.printStackTrace();  
} finally {  
    if (scanner != null) {  
        scanner.close();  
    }  
}  

使用 Java 7 之后的 try-with-resources 重构上述代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {  
    while (scanner.hasNext()) {  
        System.out.println(scanner.nextLine());  
    }  
} catch (FileNotFoundException fnfe) {  
    fnfe.printStackTrace();  
}  

当然,在需要关闭多个资源时,try-with-resources 使其变得非常简单,而使用 try-catch-finally 可能会引发许多问题。

异常使用的注意事项

  • 不要将异常定义为静态变量,因为这会导致异常栈信息错乱。每次手动抛出异常时,必须手动创建一个新的异常对象。
  • 抛出的异常信息必须具备意义。
  • 建议抛出更具体的异常,例如在字符串转换为数字格式错误时,应抛出 NumberFormatException 而不是其父类 IllegalArgumentException
  • 使用日志打印异常后,避免再次抛出异常(两者不应同时出现在同一逻辑中)。