JAVA基础——异常

前言

Java 使用异常来表示错误,并通过 try … catch 捕获异常;

Java的异常是class,并且从 Throwable 继承;

Throwable有两个体系:ErrorException

Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;

Exception又分为两大类:

  • RuntimeException以及它的子类;
  • 非RuntimeException(包括IOException、ReflectiveOperationException等等)

RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;

不推荐捕获了异常但不进行任何处理。

捕获异常

在Java中,凡是可能抛出异常的语句,都可以用try … catch捕获。把可能发生异常的语句放在 try { … } 中,然后使用catch捕获对应的Exception及其子类。

存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println("IO error");
} catch (UnsupportedEncodingException e) { // 永远捕获不到
System.out.println("Bad encoding");
}
}

对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { … }捕获并执行。

Java的try … catch机制还提供了finally语句,finally语句块保证有无错误都会执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("END");
}
}

注意finally有几个特点:

  • finally语句不是必须的,可写可不写;
  • finally总是最后执行。

使用try … catch … finally时:

  • 多个catch语句的匹配顺序非常重要,子类必须放在前面;
  • finally语句保证了有无异常都会执行,它是可选的;
  • 一个catch语句也可以匹配多个非继承关系的异常。

主动抛出异常

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

  • 创建某个Exception的实例;
  • 用throw语句抛出。
1
2
3
4
5
void process2(String s) {
if (s==null) {
throw new NullPointerException();;
}
}

调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;

捕获异常并再次抛出新的异常时,应该持有原始异常信息;

通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。

自定义异常

在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。

一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。

BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:

1
2
public class BaseException extends RuntimeException {
}

其他业务类型的异常就可以从BaseException派生:

1
2
3
4
5
6
7
public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...

自定义的BaseException应该提供多个构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BaseException extends RuntimeException {
public BaseException() {
super();
}

public BaseException(String message, Throwable cause) {
super(message, cause);
}

public BaseException(String message) {
super(message);
}

public BaseException(Throwable cause) {
super(cause);
}
}

上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。

使用断言

断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。

我们先看一个例子:

1
2
3
4
5
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
}

语句 assert x >= 0; 即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。

使用assert语句时,还可以添加一个可选的断言消息:

1
assert x >= 0 : "x must >= 0";

这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。

ava断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。

对于可恢复的程序错误,不应该使用断言。例如:

1
2
3
void sort(int[] arr) {
assert arr != null;
}

应该抛出异常并在上层捕获:

1
2
3
4
5
void sort(int[] arr) {
if (x == null) {
throw new IllegalArgumentException("array cannot be null");
}
}

JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。

要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言。所以,上述程序必须在命令行下运行才有效果:

$ java -ea Main.java
文章目录
  1. 1. 前言
  2. 2. 捕获异常
  3. 3. 主动抛出异常
  4. 4. 自定义异常
  5. 5. 使用断言
|