咱们画不出完满的圆,咱们无奈谋求到相对的美。– 罗翔
咱们无奈发明出没有 bug 的程序世界,程序世界一样不完满,总有意外状况,阻止程序实现本身的使命。因而,提供一套机制来解决异样,能力最大水平上保障程序的可靠性。
java 中的异样解决机制能够从 Exception 和 Error 讲起。
一、Exception 和 Error
Exception 和 Error 是什么呢?这事要从 Throwable 讲起。
Throwable 在 java.lang 包中,是 Java 中所有异样和谬误的超类,Exception 和 Error 都继承了 Throwable。应用 throw,catch 等关键字的时候,会被判断是不是 Throwable 的子类。
其中,Exception 通常指程序可预知的意外状况,常见的 NPE 就属于该类;
对应的,Error 指的是不可意料的,难以解决的状况,通常是程序所在环境(JVM)出了重大问题,例如熟知的 OOM。
这些谬误类型有什么区别呢?解决形式是不是一样呢?
二、checked vs unchecked
所有的异样和谬误,都能够归为查看型异样(checked Exception)和非查看型异样(unchecked Exception)。
对于异样的场景,越早进行解决,通常收益越大,能在编译期就进行检测的,收益就大于在运行时进行解决。
查看型异样,就是编译时可能进行检测的异样,例如 IOException,当文件操作的时候,编译器就会揭示进行异样解决,这里的 FileNotFoundException 就是检测型的 IO 异样,继承于 IOException。
public void testFile(String fileName){
try {BufferedReader in = new BufferedReader(new FileReader(fileName));
} catch (FileNotFoundException e) {e.printStackTrace();
}
}
然而,编译时能提醒的异样总归是无限的,大部分无奈进行无效提醒,例如 NullPointerException。
所以,Error 和 RuntimeException 类统称为非查看型异样,无奈在编写时进行检测提醒,而其余的都是属于查看型异样。
查看型的异样,有点像免责申明,提醒使用者应用时有肯定的危险,并给出了危险类型,以便使用者做出应答计划。非查看型,是无奈提前预知的危险,不晓得要产生什么,也不晓得何时产生。
总之,两类维度,从不同阶段来说,分成了查看型和非查看型,以便用不同的形式解决,而从重要水平来说,又分为异样和谬误,来表白其严重性。
那当异样理论产生的时候,JVM 是如何解决这些异样的呢?
三、解决机制
简略来说,用了一张异样表来治理异样,记录了监听的代码块,对应的异样类型,以及异样产生时要跳转的指标代码(其实就是 goto 语句)。如下所示,代表的是 0~7 产生 ClassNotFoundException 异样的时候,跳转到 17 进行解决,产生 Exception 的时候,跳转到 36 进行解决。
这里有一个重要的知识点,就是 finally 是如何体现的?如何保障的 finally 的代码肯定能执行呢?
编译的时候,finally 外面的代码被 copy 到了其它代码块外面,放到了代码块的 return 之前,比方上面这个例子,在编译后,在字节码层面就相似第二种模式。
public void test(String name){
try {Class.forName(name);
} catch (ClassNotFoundException e) {System.out.println("catch ClassNotFoundException");
} catch (Exception e){System.out.println("catch Exception");
} finally {System.out.println("finally code");
}
}
public void test(String name){
try {Class.forName(name);
System.out.println("finally code");
} catch (ClassNotFoundException e) {System.out.println("catch ClassNotFoundException");
System.out.println("finally code");
} catch (Exception e){System.out.println("catch Exception");
System.out.println("finally code");
}
}
所以这里有个比拟诡异的中央,如果 finally 外面写了 return,就会导致其余代码块外面的 return 和 throw 都不失效了,所以养成良好的习惯,防止在 finally 写 return,throw 等中断语句,不然会很难了解,不晓得产生了啥。
值得一提的是,java 7 后引入了语法糖 try-with-resource,看个文件操作的例子,实例化输入输出流,在 finally 外面须要进行很繁冗的敞开连贯的解决。
public void testFile(String fileInputName,String fileOutputName){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {bis = new BufferedInputStream(new FileInputStream(fileInputName));
bos = new BufferedOutputStream(new FileOutputStream(fileOutputName));
} catch (FileNotFoundException e) {e.printStackTrace();
} finally {if (bis != null) {
try {bis.close();
} catch (IOException e) {e.printStackTrace();
} finally {if (bos != null) {
try {bos.close();
} catch (IOException e) {e.printStackTrace();
}
}
}
}
}
}
try-with-resources 只须要这样写,很整洁优雅,写在 try 外面的资源,产生异样的时候,会主动敞开申请的资源连贯。而且这样写还有一个劣势,上述例子中,如果 finally 外面的代码产生了异样,就会笼罩原有的异样,而新写法令不会,而是通过 addSuppressed 的形式,将敞开连贯的异样加到原有的异样外面。
public void testFile(String fileInputName,String fileOutputName){try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileInputName));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileOutputName))) {//todo} catch (IOException e) {e.printStackTrace();
}
}
这里有几个要害的知识点,一个是异样表,感兴趣的同学能够看看编译后的字节码文件,一个是 finally 的实现原理,实质上就是 copy 代码,了解这个后很多相干面试题目就迎刃而解,最初,是新的写法,让资源管理更优雅,而且解决了异样被克制的问题,让呈现问题后,排查效率更加高效。
四、经典例子
晓得了基础知识后,剖析下 ClassNotFoundException
和 NoClassDefFoundError
这个经典的例子,加深下了解。
ClassNotFoundException,是一个查看型异样,编写 Class.forName() 的时候,会提醒进行异样解决。当 JVM 加载 class 文件到办法区的时候,如果没有找到该类,就会抛出该异样。一般来说,是输出的名称有问题导致的。
NoClassDefFoundError,是一个 Error,个别是 new 一个对象的时候,找不到类的定义导致的,也就是编译的时候还有该文件,然而运行的时候却找不到了。能呈现这种状况的,个别是环境上出问题了,例如编译好的 jar 被毁坏了等等因素。
五、利用准则
在平时写业务代码的时候,有什么样的领导准则呢?接下来介绍一些比拟重要的准则。
最值得提的,是 Throw early, catch late 准则,适用性比拟广。
首先是 throw 要尽量提前,比方要读取文件的时候,提前判断文件名称是否为空,尽早终止流程,比照如下两种写法,第二种可能更快的定位到问题,第一种的堆栈信息可能会很长,很一眼就看出问题所在。
public void test(String className){
try {Class.forName(className);
} catch (ClassNotFoundException e) {e.printStackTrace();
}
}
public void test(String className){
try {if (className == null) {throw new NullPointerException();
}
Class.forName(className);
} catch (ClassNotFoundException e) {e.printStackTrace();
}
}
这个思路也能够利用到写业务代码的时候,能够提前进行参数校验,业务校验,再执行具体的业务行为。
甚至,生存上也一样,出门前,查看”本领钥钱“,防止上地铁时没带手机的难堪。
其次,catch later 表白的是捕捉异样后,如果不晓得如何解决,就往下层抛,而不是吞掉异样,或者轻易解决。因为在更高的档次,会有更多的信息来解决异样。好比一个案件如果在市外面无奈解决,该当到省外面解决。
说到 catch 后的解决,这里也有几个要点须要留神下:
1、不要吞掉异样,无奈解决要往上抛;
2、不调用 e.printStackTrace(),因为是规范谬误输入;
3、不 catch Exception 或 throwable;
4、catch 多个类型要留神程序,由小到大,顺次解决。
还有就是不要用异样来做业务的流程管制,尽管能达到目标,然而可读性不高,保护效率低,另外一个是运行效率也低,因为 try-catch 是须要额定的开销,比 if else 判断效率低很多,且每实例化一个 Exception,都会对堆栈进行快照,这是个很重的动作。
六、总结
讲了这么多,总的来说,java 这套异样解决机制的劣势在哪里呢?
首先,设想下如果没有这套机制会怎么样,例如 C 语言,就没有专门的异样解决机制,异样体现在返回值外面,这样的做法会让解决异样的逻辑侵入业务代码,升高代码的可读性。
其次,如果不对异样进行封装解决,对用户极其不敌对,代码的业余报错对用户来说是不可了解的,这种做法会升高产品的体验。
七、扩大
最初,留几个思考题:
1、业务开发中,有必要定义查看型异样吗?为什么?
2、像 lambda 这种反应式编程,如何解决异样?
3、业务开发中,用拦截器对立拦挡接口的异样进行解决,这样的形式是好是坏呢?