Java核心知识体系3:深入分析异常机制
1 什么是异常
异常是指程序在运行过程中发生的,由于外部问题导致的运行异常事件,如:网络连接失败、文件找不到、非法参数、未将对象引用设置到对象的实例(空指针)等。异常是一个事件行为,在程序运行期间触发,并打断程序的运行。Java 作为面向对象的编程语言,它的异常是Throwable子类的对象的实例,当程序存在不健全的条件,且条件都满足的时候,就会触发错误并出现异常。
2 异常的分类
从Java异常类的整体层次结构,可以看出异常的具体分类:
2.1 异常抛出类型(Throwable)
在Java语言中,Throwable类 是所有的错误与异常的最顶层超类,其他的异常类都继承于该父类。它包含两个子类:Error(错误)和 Exception(异常),用于表达发生异常情况的类型。Throwable 包含了其线程创建时线程执行堆栈的快照,并提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
2.2 错误类型(Error)
Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。通常情况为应用程序 ,"不应该被捕获或者处理的验证异常"。此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。在 Java中,错误通过Error的子类描述。
2.3 异常类型(Exception)
Exception以及它的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。Exception 这种异常又分为两类:运行时异常和编译时异常。
2.3.1 运行时异常
都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
2.3.2 非运行时异常 (编译异常)
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
2.3.3 检查性异常(checked exception)
正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。除了Error 和 RuntimeException的其它异常。Java语言强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。类似如SQLException,IOException,ClassNotFoundException 等。常见的检查性异常如下:
异常 | 描述 |
Classnotfoundexception | 当应用程序试图加数一个,通过名字查找时发现没有该的定义时,抛出该异常 |
Clonenotsupportedexcept | 当去克一个对象时,发现该对象没有实现Cloneable接口时,抛出该异常 |
lllegalaccessexception | 当应用程序尝试通过反射的方式来访类、成员变量或调用方法时,却无法访问这些类、成员变量或方法的定义时,抛出该异常 |
Instantiationexception | 当试图使用 Class:类中的 newinstance方法創建一个类的实例,而制定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常 |
Interruptedexception | 个线程被另一个线程中断时,抛出该异常 |
NosuchFieldexception | 当拢不到指定的变量字段时,抛出该异常 |
NosuchMethodexception | 当我不到指定的类方法时,抛出该异常 |
2.3.4 非检查性异常(checked exception)
包括运行时异常(RuntimeException与其子类)和错误(Error)及其子类。Java语言在编译时,不会提示和发现这样的异常,不要求在程序中处理这些异常。所以我们可以在程序中编写代码来处理(使用try…catch…finally)这样的异常,也可以不做任何处理。但是这种错误或异常,一般来说是程序逻辑错误导致的异常,所以我们应该修正代码,而不是通过异常处理器处理。常见的非检查性异常如下:
异常 | 描述 |
Arithmeticexception | 当出现异常的运算条件时,抛出异常。例如,一个整数“除以零”时,抛出此美的一个实例 |
Arrayindexoutofboundsexcep | 用非法索引访问数组时跑出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引异常描述 |
Arraystoreexception | 试图将错误类型的对象存储到一个对象数组时,抛出的异常 |
Classcastexception | 试图将对象强制转换为不是同一个类型或其子类的实例时,抛出的异常 |
Illegalargumentexception | 当向一个方法传递非法或不正确的参数时,抛出该异常 |
IllegalmonitorstateException | 当某一线程已经试图等待对象的监视器,或者通知其他正在等待该对象监视器的线程,而该线程本身没有获得指定监视器时抛出该异常 |
Illegalstateexception | 在非法或不适当的时间调用方法时产生的信号。或者说Java环境或应用程序没有处于请求操作所要求的适当状态下 |
IllegalthreadstateException | 线程没有处于请求操作所要求的适当状态时,抛出该异常。 |
Indexoutofboundsexception | 当某种排序的索引超出范围时抛出的异常,例如,一个数组,字符串或一个向量的排序等 |
Negativearraysizeexception | 如果应用程序试图创建大小为负的数组时,抛出该异常 |
Nullpointerexception | 当应用程序在需要操作对象的时候而获得的对象实例是nu时抛出该异常 |
Numberformatexception | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringindexoutofboundsExcept | 此异常由 String方法抛出,说明索引为负或者超出了字符串的大小 |
3 异常基础详解
3.1 异常关键字
- try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
- catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
- finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
- throw – 用于抛出异常。
- throws – 用在方法签名中,用于声明该方法可能抛出的异常。
3.2 throws-异常的显示声明
在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:
public static void yourMethod() throws Exception{ //todo 业务逻辑}
注意:若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
private static void readFile(String filePath) throws IOException { File file = new File(filePath);
String result; BufferedReader reader = new BufferedReader(new FileReader(file)); while((result = reader.readLine())!=null) {
System.out.println(result);
}
reader.close();
}
Throws抛出异常的规则:
- 如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
- 必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误
- 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
3.3 throw-抛出异常
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:
public static double yourMethod(int value){ if(value < 0) { throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
} return 6.0 / value;
}
大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
private static void readFile(String filePath) throws MyException {
try { // code
} catch (IOException e) { MyException ex = new MyException("read file failed.");
ex.initCause(e); throw ex;
}
}
3.4 异常的自定义
习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如上面用到的自定义MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){ super(msg);
} // ...}
3.5 异常的捕获
异常捕获处理的方法通常有:
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
3.5.1 try-catch
在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理
private static void readFile(String filePath) { try { // code
} catch (FileNotFoundException e) { // handle FileNotFoundException
} catch (IOException e){ // handle IOException
}
}
同一个 catch 也可以捕获多种类型异常,用 | 隔开
private static void readFile(String filePath) { try { // code
} catch (FileNotFoundException | UnknownHostException e) { // handle FileNotFoundException or UnknownHostException
} catch (IOException e){ // handle IOException
}
}
3.5.2 try-catch-finally
- 常规语法
try {
//执行程序代码,可能会出现异常 } catch(Exception e) {
//捕获异常并处理 } finally { //必执行的代码}
- 执行的顺序
- 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
- 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
- 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
- 无异常情况 ,catch 模块被忽略,先执行业务逻辑,再执行finally。
- 异常情况,假设执行到业务逻辑2的时候,出现故障异常,则业务逻辑3没有执行,直接执行catch,最后再执行finally。
- 一个完整的例子
private static void readFile(String filePath) throws MyException { File file = new File(filePath);
String result; BufferedReader reader = null; try {
reader = new BufferedReader(new FileReader(file)); while((result = reader.readLine())!=null) {
System.out.println(result);
}
} catch (IOException e) {
System.out.println("readFile method catch block."); MyException ex = new MyException("read file failed.");
ex.initCause(e); throw ex;
} finally {
System.out.println("readFile method finally block."); if (null != reader) { try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.5.3 try-finally
也可以直接用try-finally。try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句。try块没有引发异常,则执行完try块就执行finally语句。try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等。
//以Lock加锁为例,演示try-finallyReentrantLock lock = new ReentrantLock();try { //需要加锁的代码} finally { lock.unlock(); //保证锁一定被释放}
finally遇见如下情况不会执行
- 在前面的代码中用了System.exit()退出程序。
- finally语句块中发生了异常。
- 程序所在的线程死亡。
- 关闭CPU。
3.5.4 try-with-resource
上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。
- 代码实现
private static void tryWithResourceTest(){ try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){ // code
} catch (IOException e){ // handle exception
}
}
- 看下Scanner
public final class Scanner implements Iterator<String>, Closeable { // ...}public interface Closeable extends AutoCloseable { public void close() throws IOException;
}
try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。
3.6 异常总结
- try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。
- try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
- finally语句块中的代码一定会被执行,常用于回收资源 。
- throws:声明一个异常,告知方法调用者。
- throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。Java编程思想一书中,对异常的总结。
- 在恰当的级别处理问题。(在知道该如何处理的情况下捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
- 让类库和程序更安全。
3.7 常用的异常
在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要程序员进行捕获处理或声明抛出,有的是由Java虚拟机自动进行捕获处理。Java中常见的异常类:
- RuntimeException
- java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
- java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
- java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
- java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
- java.lang.NegativeArraySizeException 数组长度为负异常
- java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常
- java.lang.SecurityException 安全性异常
- java.lang.IllegalArgumentException 非法参数异常
- IOException
- IOException:操作输入流和输出流时可能出现的异常。
- EOFException 文件已结束异常
- FileNotFoundException 文件未找到异常
- 其他
- ClassCastException 类型转换异常类
- ArrayStoreException 数组中包含不兼容的值抛出的异常
- SQLException 操作数据库异常类
- NoSuchFieldException 字段未找到异常
- NoSuchMethodException 方法未找到抛出的异常
- NumberFormatException 字符串转换为数字抛出的异常
- StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常
- IllegalAccessException 不允许访问某类异常
- InstantiationException 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常
4 异常实践
当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。这里给出几个被很多团队使用的异常处理最佳实践。
4.1 只针对不正常的情况才使用异常
异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。
比如,在解析字符串形式的数字时,可能存在数字格式错误,不得通过catch Exception来实现
- 代码1
if (obj != null) { //...}
- 代码2
try {
obj.method();
} catch (NullPointerException e) { //...}
主要原因有三点:
- 异常机制的设计初衷是用于不正常的情况,所以很少会会JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的。
- 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化。
- 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。
4.2 在 finally 块中清理资源或者使用 try-with-resource 语句
当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。
- 错误示例
public void doNotCloseResourceInTry() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt");
inputStream = new FileInputStream(file); // use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。
- 方法一:使用 finally 代码块与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。
public void closeResourceInFinally() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt");
inputStream = new FileInputStream(file); // use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally { if (inputStream != null) { try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
- 方法二:Java 7 的 try-with-resource 语法如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
public void automaticallyCloseResource() { File file = new File("./tmp.txt"); try (FileInputStream inputStream = new FileInputStream(file);) { // use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
4.3 尽量使用标准的异常
重用现有的异常有几个好处:
- 它使得你的API更加易于学习和使用,因为它与程序员原来已经熟悉的习惯用法是一致的。
- 对于用到这些API的程序而言,它们的可读性更好,因为它们不会充斥着程序员不熟悉的异常。
- 异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。Java标准异常中有几个是经常被使用的异常。如下表格:
异常 | 使用场合 |
IllegalArgumentException | 参数的值不合适 |
IllegalStateException | 参数的状态不合适 |
NullPointerException | 在null被禁止的情况下参数值为null |
IndexOutOfBoundsException | 下标越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,对象检测到并发修改 |
UnsupportedOperationException | 对象不支持客户请求的方法 |
虽然它们是Java平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。例如,如果你要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是非常合适的。如果一个异常满足你的需要,则不要犹豫,使用就可以,不过你一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是名字的基础上。最后,一定要清楚,选择重用哪一种异常并没有必须遵循的规则。例如,考虑纸牌对象的情形,假设有一个用于发牌操作的方法,它的参数(handSize)是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副牌的剩余张数。那么这种情形既可以被解释为IllegalArgumentException(handSize的值太大),也可以被解释为IllegalStateException(相对客户的请求而言,纸牌对象的纸牌太少)。
4.4 对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。
/**
* Method description
*
* @throws MyBusinessException - businuess exception description
*/public void doSomething(String input) throws MyBusinessException { // ...}
同时,在抛出MyBusinessException 异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
4.5 优先捕获最具体的异常
大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。
但问题在于,只有匹配异常的第一个 catch 块会被执行。因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。
public void catchMostSpecificExceptionFirst() { try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
4.6 不让异常影响程序的流程
不使用异常控制程序原本的执行流程,更多的试试用程序本身的判断,比如while、if,避免因为异常的执行影响程序原本的性能。
4.7 不要在finally块中使用return。
try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。如下是一个反例:
private int x = 0;public int checkReturn() { try { // x等于1,此处不返回
return ++x;
} finally { // 返回的结果是2
return ++x;
}
}
5 总结
这边详细介绍了异常的概念、原理,以及在应用中的一些小结。异常的能力是我们快速定位程序错误的重要手段之一,也是我们不断优化程序,提高程序健壮性的依据,所以熟练掌握异常的使用是非常有必要的。
文章转载自公众号: 架构与思维