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
13public 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
8private 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-else
和try-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
29public 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-else
和try-catch
的耗时,一般来说if-else
的耗时总会比try-catch
的耗时短
Java 异常处理消耗性能的地方: - try-catch块影响 JVM 的优化,影响了指令重排序带来的优化 - 异常对象实例需要保存堆栈快照等信息,开销较大 所以建议仅捕获可能出现异常的必要的代码段,不要用 try-catch 包住整个代码段,也不要用 try-catch 来控制代码流程,因为 try-catch 远没有 if-else 和 switch 的效率高
高效主流的异常处理框架 - 设计一个通用的继承自 RuntimeException 的异常 AppException 来统一处理 - 其余异常都统一转译为上述异常 AppException - 在 catch 之后,抛出上述异常 AppException 的子类,并提供足以定位异常位置的业务友好的信息,而非之前一大堆无关的堆栈信息 - 最后由前端接收 AppException 做统一处理