Java 异常

什么是异常

Java语言中,程序执行中发生的不正常情况称为“异常”。(开发过程中的语法错误和逻辑错误不是异常)

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。

Throwable 类有两个重要的子类:

  • Exception(异常):因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (非受检查异常,可以不处理)。

    • 如:空指针访问、试图读取不存在的文件、网络连接中断等等。
  • Error(错误)Error属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

异常体系图

异常体系图

受检异常和非受检异常

受检查异常(Checked Exception):Java 代码在编译过程中,如果受检查异常没有被捕获catch或者抛出throws处理的话,就没办法通过编译。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。

非受检查异常(Unchecked Exception):Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException及其子类都统称为非受检查异常,常见的有:

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

Throwable类常用方法

String getMessage():返回异常发生时的简要描述

1
String toString()`:返回异常发生时的详细信息`String

getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同

void printStackTrace():在控制台上打印Throwable对象封装的异常信息

异常处理

try-catch-finally:程序员在代码中捕获发生的异常,自行处理。

throws:将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是JVM。

try-catch-finally处理机制示意图

throws处理机制示意图

try-catch

Java提供try-catch块来处理异常。

try用于包含可能出错得代码;catch块用于处理try块中发生的异常。

使用细节

  1. 如果异常发生了,异常发生后面的代码不会执行,直接进入到catch块;
  2. 如果异常没有发生,则顺序执行try的代码块,不会进入到catch
  3. 如果希望不管是否发生异常,都执行某段代码(比如关闭连接、释放资源等),则使用finally{}
  4. 可以有多个catch语句,捕获不同的异常(进行不同的业务处理),要求子类异常在前,要求父类异常在后(如Exception要在NullPointerException后面)。如果发生异常,只会匹配一个catch
  5. 可以进行try-finally配合使用,相当于没有捕获异常,因此执行完finally后程序会直接退出。(应用场景:执行一段代码,不管是否发生异常,都必须执行某个业务逻辑。)

执行顺序

  1. 如果没有出现异常,则执行try块中所有语句,不执行catch块中语句,最后还需要执行finally里面的语句(如果有finally);
  2. 如果出现异常,则直接从异常语句直接跳到catch块中的语句,如果有finally,最后还需要执行finally中的语句
  3. 当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。(示例1);
  4. 由于异常已被处理,程序不会崩溃,继续执行下去,因此还要执行try-catch-finally后面的代码

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static int method() {
int i = 1;
try {
i++; // i=2
String names = new String[3];
if (names[i].equals("tom")) { // 空指针异常
System.out.printin(names[1]);
} else {
names[3] = "jerry";
}
return 1;
} catch (ArrayIndexOutofBoundsException e) {
return 2;
} catch (NullPointerException e) {
return ++i; //i=3 ->
// 底层会把值保存在临时变量temp=3
// (由于return不能马上执行,需要先执行finally块的语句)
} finally {
++i; // i=4
System.out.println("i=" + i); // i=4
}
}
public static void main(string[] args) {
System.out.printin(method());
}

输出:

1
2
i=4
3

注意:不要在**finally**语句块中使用**return**!try/catch语句和finally语句中都有return语句时,try/catch语句块中的return语句会被忽略。这是因为try/catch语句中的return返回值会先被暂存在一个本地变量中,当执行到finally语句中的return之后,这个本地变量的值就变为了finally语句中的return返回值。

throws

如果一个方法(中的语句执行时)可能产生某种异常,但是并不确定如何处理这种异常,则此方法应显式地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。

在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

使用规范

  1. 运行异常,程序中如果没有处理,默认就是throws的方式隐式处理(运行异常有默认处理机制);

  2. 如果一个方法有可能抛出多个受查异常类型,必须throws列出所有异常,使用逗号分隔

  3. 子类重写父类的方法时,对抛出异常的规定:

    1. 子类重写的方法,所抛出的异常类型要和父类抛出的异常一致或其子类型。
    2. 如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。

总之,一个方法必须声明或捕获所有可能抛出的受查异常, 而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。

示例

参考Integer.parseInt()方法,抛出异常分为两步:

1)创建某个Exception的实例;

2)用throw语句抛出。

1
2
3
4
5
6
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("null");
}
...
}

throw 和 throws 的区别

意义位置后面跟
throws异常处理的一种方式方法声明处异常类型
throw手动生成异常对象的关键字方法体中异常对象

⚠ 捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!

自定义异常

  1. 自定义类(自定义异常类名)
  2. 继承Exception(编译异常)或RuntimeException(运行异常)

常见问题

finally 中的代码一定会执行吗?

不一定。以下特殊情况下,finally块的代码也不会被执行(finally执行前):

  1. 虚拟机被终止运行
  2. 程序所在的线程死亡。
  3. 关闭 CPU。
1
2
3
4
5
6
7
8
9
10
11
12
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
// 终止当前正在运行的Java虚拟机
System.exit(1);
} finally {
System.out.println("Finally");
}
Try to do something
Catch Exception -> RuntimeException

相关issue:https://github.com/Snailclimb/JavaGuide/issues/190

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

适用范围(资源的定义): 任何实现java.lang.AutoCloseable或者java.io.Closeable的对象

关闭资源和finally块的执行顺序: 在try-with-resources语句中,任何catchfinally块在声明的资源关闭后运行

《Effective Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//读取文本文件的内容
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语句,让编译器自动为我们关闭资源。

使用try(resources) 语句改造上面的代码:

1
2
3
4
5
6
7
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块中声明多个资源。

1
2
3
4
5
6
7
8
9
10
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看**try(resource = ...)**中的对象是否实现了**java.lang.AutoCloseable**接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此,都可以用在try(resource)中。

异常使用有哪些需要注意的地方?

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
  • 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
  • ……

Java 异常
https://blog-21n.pages.dev/2022/06/29/Java-异常/
作者
Neo
发布于
2022年6月29日
许可协议