1、Exception 和 Error有什么区别?运行时异常与一般异常有什么区别?
Exception和Error都继承自java.lang.Throwable。在Java中只有Throwable的实例才可以被抛出和捕获,是异常处理机制的基本组成类型。
- Error描述了java运行时系统的内部错误和资源耗尽错误。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。应用程序不应该抛出这种类型的对象。
- Exception是程序正常运行中可以预料的意外情况,并且应该被捕获进行相应的处理。Exception分为可检查异常(checked)和不可检查异常(unchecked),可检查异常必须在代码中显示的捕获处理,这是编译期检查的一部分。不可检查异常就是运行时异常(java.lang.RuntimeException)类似IndexOutOfBoundsException、NullPointerException通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要进行捕获,并不会在编译期强制要求。
之所以区分 checked/unchecked exception,JAVA的设计思想是区分从类/方法设计者角度来看两种不同的异常:
一种是设计者认为这个方法在使用过程中使用者能够处理的异常,这些往往作为checked exception。
比如一个IO系统的设计者会认为诸如物理文件不存在或者介质无法读取等异常时很可能发生,而使用者完全可能捕获这个异常,通过让用户 重新输入文件名等方式重新进行这个操作,也就是说,
这是一个可恢复的操作。所以我会在诸如 read()/write()等操作中throw 一个 IOException(checked exception)。
第二种是设计者认为使用者不能够处理的异常,比如我写一个函数要求传入的参数是个正数,那么当我发现使用者传 了个负数进来时,合理的预期是程序中出bug了。如果我抛出一个异常描述这件事,
即使我要求调用者捕获这个异常,他肯定也不知道该怎么办(总不能随便传一 个正数进来吧)。这时候我就会抛出一个IllegalArgumentException(uncheck exception),这里面的潜台词
是:小子,我知道你也是帮人背黑锅的,处理不了这个,你还是交给你的领导(调用你的程序)去处理这个异常吧。 同理,当JVM发现除数为0时,抛出的ArithmeticException也是一个
unchecked exception。
从这里可以看出,checked exception和 unchecked exception的根本区别在于设计者认为使用者是否能够并且应该处理这个异常。不幸的是,由于Java使用者水平的参差不齐,大量的 unchecked
exception该被设计成了checked exception,而对于真正的checked exception,又有太多被catch了之后啥都不作就悄无声息了。尤其是不声不响吞噬exception的行为,不但达不到设计者本来的
要求(进行 恢复处理),甚至问题更大(连 unchecked exception那种最后报错的效果都没了)。
2、常见的Error异常
java.lang.AbstractMethodError
抽象方法错误。当应用试图调用抽象方法时抛出。 java.lang.AssertionError
断言错。用来指示一个断言失败的情况。 java.lang.ClassCircularityError
类循环依赖错误。在初始化一个类时,若检测到类之间循环依赖则抛出该异常。 java.lang.ClassFormatError
类格式错误。当Java虚拟机试图从一个文件中读取Java类,而检测到该文件的内容不符合类的有效格式时抛出。 java.lang.Error
错误。是所有错误的基类,用于标识严重的程序运行问题。这些问题通常描述一些不应被应用程序捕获的反常情况。 java.lang.ExceptionInInitializerError
初始化程序错误。当执行一个类的静态初始化程序的过程中,发生了异常时抛出。静态初始化程序是指直接包含于类中的static语句段。 java.lang.IllegalAccessError
违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。 java.lang.IncompatibleClassChangeError
不兼容的类变化错误。当正在执行的方法所依赖的类定义发生了不兼容的改变时,抛出该异常。一般在修改了应用中的某些类的声明定义而没有对整个应用重新编译而直接运行的情况下,容易引发该错误。 java.lang.InstantiationError
实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常. java.lang.InternalError
内部错误。用于指示Java虚拟机发生了内部错误。 java.lang.LinkageError
链接错误。该错误及其所有子类指示某个类依赖于另外一些类,在该类编译之后,被依赖的类改变了其类定义而没有重新编译所有的类,进而引发错误的情况。 java.lang.NoClassDefFoundError
未找到类定义错误。当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误。 java.lang.NoSuchFieldError
域不存在错误。当应用试图访问或者修改某类的某个域,而该类的定义中没有该域的定义时抛出该错误。 java.lang.NoSuchMethodError
方法不存在错误。当应用试图调用某类的某个方法,而该类的定义中没有该方法的定义时抛出该错误。 java.lang.OutOfMemoryError
内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 java.lang.StackOverflowError
堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出时抛出该错误。 java.lang.ThreadDeath
线程结束。当调用Thread类的stop方法时抛出该错误,用于指示线程结束。 java.lang.UnknownError
未知错误。用于指示Java虚拟机发生了未知严重错误的情况。 java.lang.UnsatisfiedLinkError
未满足的链接错误。当Java虚拟机未找到某个类的声明为native方法的本机语言定义时抛出。 java.lang.UnsupportedClassVersionError
不支持的类版本错误。当Java虚拟机试图从读取某个类文件,但是发现该文件的主、次版本号不被当前Java虚拟机支持的时候,抛出该错误。 java.lang.VerifyError
验证错误。当验证器检测到某个类文件中存在内部不兼容或者安全问题时抛出该错误。 java.lang.VirtualMachineError
虚拟机错误。用于指示虚拟机被破坏或者继续执行操作所需的资源不足的情况。
3、常见的可检查异常
java.lang.Exception
根异常。用以描述应用程序希望捕获的情况。 java.lang.ClassNotFoundException
找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常 java.lang.IllegalAccessException
当应用程序试图反射性地创建一个实例(而不是数组)、设置或获取一个字段,或者调用一个方法,但当前正在执行的方法无法访问指定类、字段、方法或构造方法的定义时,抛出 IllegalAccessException java.lang.IOException
当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通用类。 java.lang.SQLException
提供关于数据库访问错误或其他错误信息的异常。 java.lang.InterruptedException
线程在等待,睡眠或以其他方式占用时抛出,线程在活动之前或活动期间中断。
4、常见的运行时异常
java.lang.RuntimeException
运行时异常。是所有Java虚拟机正常操作期间可以被抛出的异常的父类。 java.lang.ClassCastException
当试图将对象强制转换为不是实例的子类时,抛出该异常。 java.lang.IllegalArgumentException
抛出的异常表明向方法传递了一个不合法或不正确的参数。 java.lang.IndexOutOfBoundsException
指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 java.lang.NullPointerException
当应用程序试图在需要对象的地方使用 null 时,抛出该异常。
5、 检查代码反映了异常处理中哪些不当之处?
public static void main(String[] args) {
try{
// 业务代码
Thread.sleep(1000L);
} catch (Exception e){
//Ignore it
}
}
这段代码违反了异常处理的两个原则:
第一:尽量不要捕获Exception这样的通用异常,应该捕获特定异常Thread.sleep()抛出的是java.lang.InterruptedException,另外我们也要保证程序不会捕获到我们不希望捕获的异常,例如运行时异常RuntimeException。另外除非特殊情况不要去捕获Throwable或者Error,这样很难保证我们能够正常程序处理java.lang.OutOfMemoryError;
第二:不要生吞(swallow)异常,这样很可能导致非常难以诊断的诡异情况;
public static void main(String[] args) {
try{
// 业务代码
} catch (Exception e){
e.printStackTrace();
}
}
这段代码如果作为实验代码没有问题,如果作为生产代码就有问题了。
java.lang.Throwable中的printStackTrace()方法是将此Throwable和其追溯打印到标准错误流。 此方法在错误输出流上为该Throwable
对象打印一个堆栈跟踪,该值为字段System.err
的值。
问题就出在这里,在稍微复杂的生产环境中,标准出错(stderr)不是一个合适的输出选项,因为你很难判断出到底输出到哪了。最好使用产品日志,详细的输入到日志系统中。
public void readPreferences(String fileName){
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences fil...
}
Throw early,catch late 原则
如果参数fileName为null,则会抛出java.lang.NullPointerException。由于没有第一时间暴露问题,堆栈信息可能非常令人费解。在生产环境可能有各种各样的场景,如果在发现问题的时候,能第一时间抛出,能更加清晰的反应问题。下面是我们修改后的代码,让Throw early,异常信息就非常直观了:
public void readPreferences(String fileName){
Objects. requireNonNull(fileName);
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences fil...
}
catch late 我们一般有两种处理方式,一种就是生吞,本质其实就是掩盖异常。另一种就是保留原有异常的cause信息,直接抛出或者构建新的异常抛出。在更高层面因为有了清晰的业务逻辑,往往更清楚怎么处理。
6、自定义异常
有时候我们会根据需要自定义异常,这个时候除了保证提供足够的信息,还需要考虑两点:
第一:是否需要定义成Check Exception,这种类型的设计初衷更是为了从异常情况恢复;另外如果一个异常所表示的并不是代码本身的不足所导致的非正常状态,而是一系列应用本身也无法控制的情况,那么我们将需要使用Checked Exception。
第二:在保证诊断信息足够的同时,也需要考虑避免包含敏感信息;例如:java.net.ConnectException出错的信息是类似“Connection refused”而不包含具体的机器名,IP等。
业界有一种争论,java语言的Check Exception也许是一个设计错误:
Check Exception的假设是我们捕获了异常,然后恢复程序。但实际大多数情况不可能恢复;
Check Exception不兼容functional编程;
7、从性能角度解析Java的异常处理机制
- try-catch代码段会产生额外的性能开销,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段;于此同时通过异常来控制代码流程也不是一个好主意,远比我们通常的(if/else,switch)要低效。
- java每实例化一个Exception,都会对当时的栈进行快照,这是个相对比较重的操作。如果发生的非常频繁,这个开销可能就不能被忽略。
参考:
https://time.geekbang.org/column/article/6849
http://www.importnew.com/27834.html