Java/Java异常体系

Java/Java异常体系

Java 从诞生之初就提供了完善的异常处理机制,大大降低了编写和维护可靠程序的门槛。Java 的异常处理机制主要回答了 3 个问题: - What:异常类型回答了什么被抛出 - Where:异常堆栈跟踪回答了在哪里被抛出 - Why:异常信息回答了为什么被抛出

Java异常体系类图


Error 以及 Exception

Error 和 Exception 的区别 - Error:程序无法处理的系统错误,编译器不做检查,这种情况应该终止程序 - Exception:程序可以处理的异常,捕获后可以恢复,应尽可能在程序内处理这种异常

Exception 的分类: - RuntimeException:这类异常是不可预知的,程序应该通过各种判断来自行避免 - 可预知的异常,编译器强制必须校验的异常。如不哦程序不处理,则编译不通过 不要抛出异常的父类来粗化异常

从责任角度看: - Error 属于 JVM 需要承担的责任 - RuntimeException 属于程序应该负担的责任 - Checked Exception 可检查异常是 Java 编译器应该承担的责任

常见 Error 以及 Exception

  • NullPointException:空指针引用异常
  • ClassCastException:类型强制转换异常
  • IllegalArgumentException:传递非法参数异常
  • IndexOutOfBoundsException:下标越界异常
  • NumberFormatException:数字格式异常

Checked Exception - ClassNotFoundException:找不到指定 Class 的异常 - IOException:IO 操作异常

Error - NoClassDefFoundError:找不到 Class 定义的异常 - 类依赖的 Class 或者 jar 不存在 - 类文件存在,但同一个类被多个类加载器重复加载,存在于不同的域中 - 大小写问题:javac 编译是无视大小写的,很有可能编译出来的 Class 文件与想要的不一样 - StackOverflowError:深递归导致栈被耗尽而抛出的异常 - OutOfMemoryError:内存溢出异常

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExceptionAndError {
private void throwError() {
throw new StackOverflowError();
}

private void throwRuntimeException() {
throw new RuntimeException();
}

private void throwCheckedException() {
throw new IOException();
}
}
上面的代码里,第三个方法体throwCheckedException()内的异常属于 CheckedException(可检查异常),编译器会强制要求捕获该异常,否则编译不能通过。 可以使用try-catch把异常捕获,在catch里面增加相应的处理逻辑,具体的处理逻辑需要依据业务需要而定,一般的原则是理解这种Exception的原因,并结合实际业务处理,如下所示。
1
2
3
4
5
6
7
8
private void throwCheckedException() {
try {
throw new IOException();
} catch (IOException e) {
//在这里根据业务需要处理异常
e.printStackTrace();
}
}

在实际场景中,一个项目是由很多模块组成的。假设上面的代码是模块 A 的,如果 B 调用了 A 的方法产生了异常,那么异常不应该由 A 使用try-catch捕获,因为A 并不了解 B 的业务,因此 A 难以根据 B 的业务来处理Exception,因此最佳实践是 A 把异常继续往上抛出,由上层调用者来处理异常,如下所示。

1
2
3
4
//最佳实践:把异常往上抛出(并且不要抛出异常的父类,这样会粗化异常,丢失异常细节信息),由调用者来处理异常
private void throwCheckedException() throws IOException {
throw new IOException();
}

在引发异常时,会在堆上创建异常对象,对象包含异常类型和程序运行状态等异常信息。然后中断当前正在执行的流程,由运行时系统去寻找合适的异常处理器。在catch代码块 return 之前,会先执行 finally 代码块的语句,所以 finally 里的 return 会先于 catch 代码块的 return。

捕获异常时不要捕获异常的父类,会给团队中其他同事掩盖具体的异常信息,并且可能捕获到不希望捕获的异常。 不要生吞异常,如果不在第一时间抛出异常,程序会以不可控的方式结束,并且难以定位出错的地方

Java 异常的处理原则 - 具体明确:抛出的异常应该能够通过异常类名和 message 准确说明异常的类型和产生异常的原因 - 提早抛出:应尽可能早地发现并抛出异常,便于精确定位问题 - 延迟捕获:异常的捕获和处理应该尽可能延迟,让掌握更多信息的作用域处理异常

下面来对比if-elsetry-catch的性能

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
26
27
28
29
public class ExceptionPerformance {
public static void testException(String[] array) {
try {
System.out.println(array[0]);
} catch (Exception e) {
System.out.println("array can not be null");
}
}

public static void testIf(String[] array) {
if (array != null) {
System.out.println(array[0]);
} else {
System.out.println("array can not be null");
}
}

public static void main(String[] args) {
long start = System.nanoTime();
testIf(null);
long end = System.nanoTime();
System.out.println("if-else handle time:" + (end - start));

start = System.nanoTime();
testException(null);
end = System.nanoTime();
System.out.println("Exception handle time:" + (end - start));
}
}
上面代码对比了if-elsetry-catch的耗时,一般来说if-else的耗时总会比try-catch的耗时短

Java 异常处理消耗性能的地方: - try-catch块影响 JVM 的优化,影响了指令重排序带来的优化 - 异常对象实例需要保存堆栈快照等信息,开销较大 所以建议仅捕获可能出现异常的必要的代码段,不要用 try-catch 包住整个代码段,也不要用 try-catch 来控制代码流程,因为 try-catch 远没有 if-else 和 switch 的效率高

高效主流的异常处理框架 - 设计一个通用的继承自 RuntimeException 的异常 AppException 来统一处理 - 其余异常都统一转译为上述异常 AppException - 在 catch 之后,抛出上述异常 AppException 的子类,并提供足以定位异常位置的业务友好的信息,而非之前一大堆无关的堆栈信息 - 最后由前端接收 AppException 做统一处理

评论