1 异常层次结构
异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引发异常。
Java异常类层次结构图:
Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。
运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
2 JVM字节码分析异常处理机制
我们都知道 try、catch、finally语句块的执行顺序:
try-catch-finally 控制流
接下来我们从字节码的角度加深对异常机制的理解,
我们先反编译如下代码:
public class FileDemo {
private static void divideFun(int a, int b) {
b = a - b;
}
public static void main(String[] args) {
int a = 1;
int b = 3;
try {
b = a + b;
FilterInputStream filterInputStream = new BufferedInputStream(new FileInputStream("d:/a"));
} catch (FileNotFoundException e) {
System.out.println("file error");
divideFun(a, b);
} catch (RuntimeException e) {
b = a * b;
throw e;
} finally {
a = 0;
}
}
}
通过命令javap -c .\FileDemo.class 反编译.class文件得到一下输出:
PS E:\totalpalce\ContentTest\target\classes\exception> javap -c .\FileDemo.class
Compiled from "FileDemo.java"
public class exception.FileDemo {
public exception.FileDemo();
Code:
0: aload_0 //从局部变量0中装载引用类型值
1: invokespecial #1 // Method java/lang/Object."<init>":()V 根据编译时类型来调用实例方法
4: return //从方法中返回,返回值为void
public static void main(java.lang.String[]);
Code:
0: iconst_1 //将int类型常量1压入栈
1: istore_1 //将int类型值存入局部变量1 a = 1
2: iconst_3 //将int类型常量3压入栈
3: istore_2 //将int类型值存入局部变量2 b = 3
4: iload_1 //从局部变量1中装载int类型值 异常语句块 start
5: iload_2 //从局部变量2中装载int类型值
6: iadd // 执行int类型的加法 a + b
7: istore_2 //将int类型值存入局部变量2 b = a + b
8: new #2 //创建一个新对象 class java/io/BufferedInputStream
11: dup //复制栈顶部一个字长内容
12: new #3 // 创建一个新对象 class java/io/FileInputStream
15: dup //复制栈顶部一个字长内容
16: ldc #4 //把常量池中的 "d:/a" 压入栈
18: invokespecial #5 //根据编译时类型来调用实例方法 Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
21: invokespecial #6 //根据编译时类型来调用实例方法 Method java/io/BufferedInputStream."<init>":(Ljava/io/InputStream;)V
24: astore_3 //将引用类型值存入局部变量3
25: iconst_0 //将int类型常量0压入栈 异常语句块 end
26: istore_1 //将int类型值存入局部变量1 finally语句块 a = 0
27: goto 63 //无条件跳转至63
//碰到 FileNotFoundException时,跳到 30 号指令
30: astore_3 //将引用类型值存入局部变量3
31: getstatic #8 //从类中获取静态字段 Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #9 // 把常量池中的"file error"压入栈
36: invokevirtual #10 //运行时按照对象的类来调用实例方法 Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: iload_1 //从局部变量1中装载int类型值
40: iload_2 //从局部变量2中装载int类型值
41: invokestatic #11 // 调用类(静态)方法 Method divideFun:(II)V
44: iconst_0 //将int类型常量0压入栈
45: istore_1 //将int类型值存入局部变量1 finally语句块 a = 0
46: goto 63 //无条件跳转至63
//碰到 RuntimeException时,跳到 49 号指令
49: astore_3 //将引用类型值存入局部变量3
50: iload_1 //从局部变量1中装载int类型值
51: iload_2 //从局部变量2中装载int类型值
52: imul //执行int类型的乘法 a*b
53: istore_2 //将int类型值存入局部变量2 b = a * b
54: aload_3 //从局部变量3中装载引用类型值
55: athrow //抛出异常或错误 throw e
//其他未捕获异常时候,跳到 56 号指令
56: astore 4 // 将引用类型或returnAddress类型值存入局部变量
58: iconst_0 //将int类型常量0压入栈
59: istore_1 //将int类型值存入局部变量1 finally语句块 a = 0
60: aload 4 //从局部变量中装载引用类型值(refernce)
62: athrow //抛出异常或错误
63: return //从方法中返回,返回值为void
Exception table: // 异常表
from to target type
4 25 30 Class java/io/FileNotFoundException //4-25号指令中,碰到 FileNotFoundException时,跳到 30 号指令
4 25 49 Class java/lang/RuntimeException
4 25 56 any
30 44 56 any
49 58 56 any
通过以上指令,我们能够理解,生成字节码指令的时候,会有一张异常表,记录发生异常情况,跳转到哪一条指令,捕获的异常会在字节码指令中跟踪 try语句块中的代码,当出现该异常的时候跳转到相应catch内部的语句块和最后的finally语句块,如果出现的异常是我们未捕获的,则会走finally的逻辑,并抛出异常错误返回
为了更好的理解我们可以看下面字节码指令结构图:
异常字节码指令图
3 Java异常处理的一般性建议
try-catch-finally 规则 异常处理语句的语法规则:
- 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,但至少有一个块。
- 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try 块之后。
- catch 块与相应的异常类的类型相关。
- 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。即Java虚拟机会把实际抛出的异常对象依次和各个catch代码块声明的异常类型匹配,如果异常对象为某个异常类型或其子类的实例,就执行这个catch代码块,不会再执行其他的 catch代码块
- 可嵌套 try-catch-finally 结构。
- 在 try-catch-finally 结构中,可重新抛出异常。
- 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用 System.exit(int));在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。
Throws抛出异常的规则:
- 如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
- 必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误
- 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
4 异常处理优化
4. 1 Java7中IO的异常处理 try-with-resource
try-with-resources语句是一种声明了一种或多种资源的try语句。资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。
public class FileHandlerOptimize {
private static void printFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("D:/user.txt");
int data = input.read();
while (data != -1) {
System.out.print((char) data);
data = input.read();
}
} finally {
if (input != null) {
input.close();
}
}
}
//单个流关闭
private static void printFileJava7() throws IOException {
try (FileInputStream input = new FileInputStream("D:/user.txt")) {
int data = input.read();
while (data != -1) {
System.out.print((char) data);
data = input.read();
}
}
}
//如果需要对多个流自动关闭
private static void printFileJava7Multiple() throws IOException {
//你可以在一个try-with-resources语句里面声明多个资源
try (FileInputStream input = new FileInputStream("D:/user.txt");
BufferedInputStream bufferedInput = new BufferedInputStream(input)) {
int data = bufferedInput.read();
while (data != -1) {
System.out.print((char) data);
data = bufferedInput.read();
}
}
}
public static void main(String[] args) throws Exception {
//单个流处理
printFileJava7();
//多个流处理
printFileJava7Multiple();
}
}
这是java7的新特性比较简单,但是它做了一定的优化,你反编译.class文件会发现新增了一行代码如下:
private static void printFileJava7() throws IOException {
FileInputStream input = new FileInputStream("D:/user.txt");
Throwable var1 = null;
try {
for(int data = input.read(); data != -1; data = input.read()) {
System.out.print((char)data);
}
} catch (Throwable var10) {
var1 = var10;
throw var10;
} finally {
if (input != null) {
if (var1 != null) {
try {
input.close();
} catch (Throwable var9) {
var1.addSuppressed(var9);
}
} else {
input.close();
}
}
}
}
多了 var1.addSuppressed(var9); 这行代码,从Java 1.7开始,大佬们为Throwable类新增了addSuppressed方法,支持将一个异常附加到另一个异常身上,从而当出现两个以上的异常时,避免异常被屏蔽覆盖
4.2 Java异常处理模板
对如下代码进行优化:
public static void readFile() throws IOException {
byte[] buff = new byte[1024];
FileInputStream input = null;
try {
input = new FileInputStream(new File("D:/user.txt"));
while (-1 != input.read(buff)) {
System.out.println(new String(buff));
}
} catch (IOException e) {
e.getMessage();
throw e;
} finally {
if (null != input) {
try {
input.close();
} catch (IOException e) {
e.getMessage();
throw e;
}
}
}
}
注意: 上面的例子实际上存在异常丢失的隐患, 如果第一个try中出现异常, 接着在执行finally中的input.close()也出现异常, 这时main 方法只能接收到input.close的异常信息, 第一个异常会被覆盖, 导致异常信息丢失
所以正确的写法如下:
public class FileReadDemo {
public void readFile() throws ApplicationException {
byte[] buff = new byte[1024];
IOException processException = null;
FileInputStream input = null;
try {
input = new FileInputStream(new File("D:/user.txt"));
while (-1 != input.read(buff)) {
System.out.println(new String(buff));
}
} catch (IOException e) {
processException = e;
} finally {
try {
if(null != input){
input.close();
}
} catch (IOException e) {
if(null == processException){
throw new ApplicationException(e);
}else{
throw new MyException("FileInputStream close exception", processException);
}
}
if(processException !=null){
throw new MyException(processException);
}
}
}
public static void main(String[] args){
try {
readFile();
} catch (ApplicationException e) {
e.printStackTrace();
}
}
}
这种写法是不是很糟糕, 我们实际关注的代码就只是第一个try中的四行代码, 这样的缺陷很明显, 一旦系统中有多处地方要用到类似的文件处理操作时, 就需要重复的做try-catch-finally, 很明显, 这就违背了程序开发中的一个重要原则: DRY(Don’t repeat yourself), 代码重复, 不容易阅读和维护, 同时也存在一个隐患, 忘记关闭, 异常没正确处理等, 引入模板方法就可以很好的解决这个问题.
public class MyException extends RuntimeException {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public MyException(Throwable cause, String message) {
super(message, cause);
}
public MyException(Throwable suppressed, Throwable throwable, String message) {
super(message, throwable);
//多异常覆盖问题
throwable.addSuppressed(suppressed);
}
public MyException(Throwable cause) {
super(cause);
}
protected MyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
提取重复代码
public abstract class InputStreamProcessingTemplate {
public void process(String fileName) {
//防止异常信息丢失
IOException processException = null;
InputStream input = null;
try {
input = new FileInputStream(fileName);
doProcess(input);
} catch (IOException e) {
processException = e;
} finally {
processExceptionHandle(fileName, processException, input);
}
}
//override this method in a subclass, to process the stream.
public abstract void doProcess(InputStream input) throws IOException;
private static void processExceptionHandle(String fileName, IOException processException, InputStream input) {
if (input != null) {
try {
input.close();
} catch (IOException e) {
if (processException != null) {
//防止异常信息丢失
throw new MyException(processException, e, "Error message..." + fileName);
}
throw new MyException(e, "Error closing InputStream for file " + fileName);
}
}
if (processException != null) {
throw new MyException(processException, "Error processing InputStream for file " + fileName);
}
}
}
通过这种方式,我们发现我们需要处理逻辑,不需要考虑流的关闭问题
public class Test {
public static void main(String[] args) {
new InputStreamProcessingTemplate(){
@Override
public void doProcess(InputStream input) throws IOException {
byte[] buff = new byte[1024];
while(input.read(buff) != -1){
//do something with the chars...
System.out.println(new String(buff));
}
}
}.process("D:\\user.txt");
}
}
或者使用静态模板方法
public interface InputStreamProcessor {
public void process(InputStream input) throws IOException;
}
静态方法
public class InputStreamProcessingTemplate {
public static void process(String fileName, InputStreamProcessor processor) {
//防止异常信息丢失
IOException processException = null;
InputStream input = null;
try {
input = new FileInputStream(fileName);
processor.process(input);
} catch (IOException e) {
processException = e;
} finally {
processExceptionHandle(fileName, processException, input);
}
}
private static void processExceptionHandle(String fileName, IOException processException, InputStream input) {
if (input != null) {
try {
input.close();
} catch (IOException e) {
if (processException != null) {
//防止异常信息丢失
throw new MyException(processException, e, "Error message..." + fileName);
}
throw new MyException(e, "Error closing InputStream for file " + fileName);
}
}
if (processException != null) {
throw new MyException(processException, "Error processing InputStream for file " + fileName);
}
}
}
这样看上出代码更加简洁优雅
public class Test {
public static void main(String[] args) {
InputStreamProcessingTemplate.process("D:\\user.txt", input -> {
byte[] buff = new byte[1024];
while(input.read(buff) != -1){
//do something with the chars...
System.out.println(new String(buff));
}
});
}
}
4.3 java8中的异常简化
实现 Supplier<T> 提供型接口
public class MyException extends RuntimeException implements Supplier<RuntimeException> {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public MyException(Throwable cause, String message) {
super(message, cause);
}
public MyException(Throwable suppressed, Throwable throwable, String message) {
super(message, throwable);
throwable.addSuppressed(suppressed);
}
public MyException(Throwable cause) {
super(cause);
}
protected MyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
@Override public RuntimeException get() {
return this;
}
如此我们可以简化我们的NPE判断
原先我们都是这样
if (user == null) {
throw new MyException();
}
if (user.getAge() == null) {
throw new MyException();
}
现在可以这样
Optional.ofNullable(Optional.ofNullable(user).orElseThrow(MyException::new).getAge()).orElseThrow(MyException::new);
4.4 异常增强
我们在平时的软件开发中一般都是进行异常包装,包括其中的 错误码 和 错误信息,
来提示我们程序错误的原因,例如下面例子:
public void method3() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("An error occurred when trying to ...");
throw e;
}
}
public void method2() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("An error occurred when trying to ...");
throw e;
}
}
public void method1() throws EnrichableException {
if(...) throw new EnrichableException(
"ERROR1", "Original error message");
}
方法method1抛出一个异常,通过唯一的错误码标识“ERROR1”。请注意,method1()被method2()和method3()调用。尽管在method1()方法中,进行了异常信息记录,但是无论在方法method2和method3中运行的时候method1出错,封装的错误结果都是相同的,但是对于开发人员来说,确定是具体是哪一个方法出的问题这可能很重要。错误码“ERROR1”足以确定错误发生在哪里,但不知道在哪个上下文中出现的情况。
解决这个问题的方法是在异常中添加唯一的上下文错误代码,就像添加其他上下文信息一样。这里有一个例子,addInfo()方法已经被更改以适应以下情况
public void method3() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("METHOD3", "ERROR1",
"An error occurred when trying to ...");
throw e;
}
}
public void method2() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("METHOD2", "ERROR1",
"An error occurred when trying to ...");
throw e;
}
}
public void method1() throws EnrichableException {
if(...) throw new EnrichableException(
"METHOD1", "ERROR1", "Original error message");
}
这样当method1()从method3()调用时,错误标识将如下所显示:
[METHOD3:ERROR1][METHOD1:ERROR1]
我们就可以确定方法出错的位置和调用链
而异常增强,则是有这种需求场景的前提下,通过添加异常调用链信息,追踪问题
代码如下:
异常句柄
public interface ExceptionHandler {
void handle(String contextCode, String errorCode,
String errorText, Throwable t);
void raise(String contextCode, String errorCode,
String errorText);
}
异常类
public class EnrichableException extends RuntimeException {
public static final long serialVersionUID = -1;
private List<InfoItem> infoItems = new ArrayList<>();
private class InfoItem {
private String errorContext;
private String errorCode;
private String errorText;
private InfoItem(String contextCode, String errorCode, String errorText) {
this.errorContext = contextCode;
this.errorCode = errorCode;
this.errorText = errorText;
}
}
public EnrichableException(String errorContext, String errorCode, String errorMessage) {
addInfo(errorContext, errorCode, errorMessage);
}
public EnrichableException(String errorContext, String errorCode, String errorMessage, Throwable cause) {
super(cause);
addInfo(errorContext, errorCode, errorMessage);
}
public EnrichableException addInfo(String errorContext, String errorCode, String errorText) {
this.infoItems.add(new InfoItem(errorContext, errorCode, errorText));
return this;
}
public String getCode() {
StringBuilder builder = new StringBuilder();
for (int i = this.infoItems.size() - 1; i >= 0; i--) {
InfoItem info = this.infoItems.get(i);
builder.append('[');
builder.append(info.errorContext);
builder.append(':');
builder.append(info.errorCode);
builder.append(']');
}
return builder.toString();
}
@Override public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(getCode());
builder.append('\n');
//append additional context information.
for (int i = this.infoItems.size() - 1; i >= 0; i--) {
InfoItem info = this.infoItems.get(i);
builder.append('[');
builder.append(info.errorContext);
builder.append(':');
builder.append(info.errorCode);
builder.append(']');
builder.append(info.errorText);
if (i > 0) {
builder.append('\n');
}
}
//append root causes and text from this exception first.
if (getMessage() != null) {
builder.append('\n');
if (getCause() == null) {
builder.append(getMessage());
} else if (!getMessage().equals(getCause().toString())) {
builder.append(getMessage());
}
}
appendException(builder, getCause());
return builder.toString();
}
private void appendException(StringBuilder builder, Throwable throwable) {
if (throwable == null) {
return;
}
appendException(builder, throwable.getCause());
builder.append(throwable.toString());
builder.append('\n');
}
}
测试类
public class ExceptionTest {
protected ExceptionHandler exceptionHandler = new ExceptionHandler(){
@Override
public void handle(String errorContext, String errorCode,
String errorText, Throwable t){
if(! (t instanceof EnrichableException)){
throw new EnrichableException(
errorContext, errorCode, errorText, t);
} else {
((EnrichableException) t).addInfo(
errorContext, errorCode, errorText);
}
}
@Override
public void raise(String errorContext, String errorCode,
String errorText){
throw new EnrichableException(
errorContext, errorCode, errorText);
}
};
public static void main(String[] args){
ExceptionTest test = new ExceptionTest();
try{
test.level1();
} catch(Exception e){
e.printStackTrace();
}
}
public void level1(){
try{
level2();
} catch (EnrichableException e){
this.exceptionHandler.handle(
"L1", "E1", "Error in level 1, calling level 2", e);
throw e;
}
}
public void level2(){
try{
level3();
} catch (EnrichableException e){
this.exceptionHandler.handle(
"L2", "E2", "Error in level 2, calling level 3", e);
throw e;
}
}
public void level3(){
try{
level4();
} catch(Exception e){
this.exceptionHandler.handle(
"L3", "E3", "Error at level 3", e);
}
}
public void level4(){
throw new IllegalArgumentException("incorrect argument passed");
}
}
运行main方法结果如下
[L1:E1][L2:E2][L3:E3]
[L1:E1]Error in level 1, calling level 2
[L2:E2]Error in level 2, calling level 3
[L3:E3]Error at level 3
java.lang.IllegalArgumentException: incorrect argument passed
我们可以清晰的定位在哪个流程中函数调用出现了异常
Reference
Java Exception Handling
JVM 对 Java 异常的处理原理
JAVA异常处理
异常管理 - try-catch-finally异常信息丢失
深入理解Java try-with-resource
网友评论