世界上不可能存在不会出错的程序,对于一门优秀的语言,它必定具备容灾机制的特性,这样才能保证程序的可靠性。
Java在设计之初就提供了相对完善的异常处理机制,该机制大大降低了编写和维护可靠程序的门槛,或许这也是Java如今这么火爆的原因之一。
在此,先抛出两个面试中常问到的问题——Exception和Error的区别,RuntimeException和CheckedException的区别
Exception和Error都继承自Throwable类,在Java中只有继承了Throwable类型的实例才能够被抛出或捕获。
Exception和Error体现了Java平台设计者对不同异常情况的分类:
直观地说:Error是程序无法处理的错误,Exception是程序可以处理的异常。
而Exception又分为CheckedException和RuntimeException:
以下是几个常见的RuntimeException,CheckedException,Error,希望自己和读者都能够做到心里有数...(゜▽゜*)
①NullPointException - 空指针异常
②ClassCastException - 类型强制转换异常
③IllegalArgumentException - 传递非法参数异常
④IndexOutOfBoundsException - 数组越界
⑤NumberFormatException - 数字格式异常
①ClassNotFoundException - 找不到指定class的异常
②IOException - IO操作异常
①NoClassDefFoundError - 找不到class定义的错误
②StackOverflowError - 深递归导致栈被耗尽的错误
③OutOfMemoryError - 内存溢出错误
到此,再抛出一个问题:ClassNotFoundException和NoClassDefFoundError有什么区别?
首先,一个是Error一个是CheckedException。既然NoClassDefFoundError是Error的话,那问题多半是与JVM挂钩了,我们先观察下面这个非常简单的代码:
可以看到,在TestNoClassDefFoundError中调用了A的构造函数创建了一个A对象,
public class TestNoClassDefFoundError {
public static void main(String[] args) {
new A();
}
}
class A{
}
在我们点击执行main方法后,javac首先会通过我们编写的TestNoClassDefFoundError.java编译出两个.class文件(A.class和TestNoClassDefFoundError.class),然后就是JVM对我们的.class文件进行类加载了...... 也就是说,当main成功执行完毕后,我们可以获得TestNoClassDefFoundError.java和A.java这两个字节码文件。
此时我们将A.class删除,再点击main方法执行,就会报NoClassDefFoundError,也就是说JVM加载不到A.class去执行main中A的构造方法。
说完了NoClassDefFoundError,ClassNotFoundException就简单很多了,它是一个可检查异常,我们必须在编译期就得对它进行捕获。例如我们通过Class获取一个xx类的反射对象时,就需要抛出或捕获处理该异常,话不多还是说看代码吧:
public class Demo {
public static void main(String[] args) {//throws ClassNotFoundException
path = "com.xxx.xxxClass";
try {
claz = Class.forName(path);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
读到这里相信大家对NoClassDefFoundError,ClassNotFoundException的区别已经有了大概的了解了( •̀ ω •́ )
说完了各种异常,那么再来看一下Java对这些异常的处理机制
首先呢,是抛出异常:当一个方法出现错误引发异常时,方法会创建一个异常对象交付给运行时系统,该异常对象包含了异常类型和异常出现时的程序状态等异常信息。
之后就是捕获异常:在方法抛出异常后,运行时系统会去寻找合适的异常的异常处理器(ExceptionHandler),并执行其方法。若运行时系统遍历了调用栈都没有找到合适的异常处理器,则运行时系统终止,Java程序终止。
上述描述若没有看懂也没有关系,以下通过一段简单的代码对其过程进行描述:
public class ExceptionHandlerMechanism {
public static int doMethod() {
try {
int i = 10/0;//抛出一个异常对象
System.out.println(i);
}catch (ArithmeticException e){//在catch块中寻找合适的异常处理器
System.out.println("ArithmeticException");
return 10;
}catch (Exception e){
System.out.println("Exception");
return 5;
}finally {
System.out.println("finally");
return 0;
}
}
public static void main(String[] args) {
System.out.print(doMethod());
}
}
其运行过程如下:
①程序首先在int i = 10/0;的位置抛出一个异常对象
②然后运行时系统对该异常对象进行捕获操作,也就是上述过程中的寻找合适的异常的异常处理器。
③该异常对象在ArithmeticException的catch块中被捕获,并执行其中的方法。
这里注意!!!ArithmeticException的catch块中的return是不会执行的,有一种错误的说法是:ArithmeticException先执行了return,然后finnally中执行的return会把先前ArithmeticException中return的结果覆盖。因为我曾经就这么认为了很久....( ′⌒`)
④然后就是执行finally了。
其运行结果相信大家已经心里有数了:
ArithmeticException
finally
0
说完了异常的处理机制后,我们再来聊一聊异常的规范吧
①不要捕获我们不需要捕获的异常:不必要的异常的捕获和处理交给掌握更多信息的上层,说白了就是别多闲事。
②不要生吞异常:捕获了异常则必须对其进行处理,不要什么都做,生吞异常会导致程序出现问题后难以诊断问题所在,也就是别占着茅坑不拉屎。
③尽量不要捕获通用异常:能定位到FileNotFoundException就不要用IOException或Exception,这样能更加精确地定位问题的所在以及增加代码的可读性。此外,我们某些情况是需要将例如RuntimeException的异常扩散出来,错误的做法是选择捕获Exception,它会把我们不想捕获的RuntimeException也捕获了。
④对于很多子模块系统或分布式系统,采用日志持久化保存异常信息:在这类系统中,如果仅仅通过控制台打印异常,是无法找到相关异常信息的。
⑤抛出的异常能够通过其名称和message准确说明异常产生的类型和原因:这个就不用解释了
⑥仅捕获可能出现异常的必要的代码段:不要用一大个try包住整个代码段,因为try-catch的效率是远远比不上if-else/switch的。(因为try-catch块中每次遇到异常都会实例化一个Exception对象,对当时的栈进行快照进行保存等。)
关于异常差不多就写到这⑧..(●'◡'●)