Java异常是Java提供的一种辨识及响应错误的一致性机制,ava异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码愈发柔美,并提升程序健壮性。本文综合多篇文章后,总结了Java 异常的相关知识,希望可以提高你对Java中异常的认知效率。@pdai
异常的层次结构
异常指不期而至的各类状况,如:文件找不到、网络联接失败、非法参数等。异常是一个风波,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的诸多泛型描述各类不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引起异常。
Java异常类层次结构图:
Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
Throwable 包含两个泛型:Error(错误)和 Exception(异常),它们一般用于指示发生了异常情况。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等插口用于获取堆栈跟踪数据等信息。
Error(错误)
Error 类及其泛型:程序中难以处理的错误,表示运行应用程序中出现了严重的错误。
此类错误通常表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将中止线程。
这些错误是不受检异常,非代码性错误。因此,当这种错误发生时,应用程序不应当去处理这种错误。按照Java惯例,我们是不应当实现任何新的Error基类的!
Exception(异常)
程序本身可以捕获而且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
都是RuntimeException类及其泛型异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检测异常,程序中可以选择捕获处理,也可以不处理。这些异常通常是由程序逻辑错误造成的,程序应当从逻辑角度尽可能防止这类异常的发生。
运行时异常的特征是Java编译器不会检测它,也就是说,当程序中可能出现这类异常,即使没有用try-catch句子捕获它,也没有用throws谓词申明抛出它,也会编译通过。
是RuntimeException以外的异常,类型上都属于Exception类及其泛型。从程序句型角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检测异常。
可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。
正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常其实是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这些异常状况,就必须采取某种形式进行处理。
除了RuntimeException及其泛型以外,其他的Exception类及其泛型都属于可查异常。这种异常的特征是Java编译器会检测它,也就是说,当程序中可能出现这类异常,要么用try-catch句子捕获它,要么用throws谓词申明抛出它,否则编译不会通过。
包括运行时异常(RuntimeException与其泛型)和错误(Error)。
异常基础
接下来我们看下异常使用的基础。
异常关键字异常的声明(throws)
在Java中,当前执行的句子必属于某个方式,Java类库调用main方式执行开始执行程序。若方式中存在检测异常,如果不对其捕获,那必须在方式头中显式申明该异常,以便于告知方式调用者此方式有异常,需要进行处理。 在方式中申明一个异常,方法头中使用关键字throws,后面接上要申明的异常。若申明多个异常,则使用冒号分割。如下所示:
public static void method() throws IOException, FileNotFoundException{
//something statements
}
注意:若是父类的方式没有申明异常,则泛型承继方式后,也不能申明异常。
通常,应该捕获这些晓得怎样处理的异常,将不知道怎样处理的异常继续传递下去。传递异常可以在方式签名处使用 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抛出异常的规则:
异常的抛出(throw)
如果代码可能会引起某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
}
return 5.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;
}
}
异常的自定义
习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详尽描述信息的构造函数(Throwable 的 toString 方法会复印那些详尽信息,调试时很有用), 比如前面用到的自定义MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
异常的捕获
异常捕获处理的方式一般有:
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
}
}
try-catch-finally
try {
//执行程序代码,可能会出现异常
} catch(Exception e) {
//捕获异常并处理
} 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();
}
}
}
}
try-finally
可以直接用try-finally吗? 可以。
try块中造成异常,异常代码以后的句子不再执行,直接执行finally词句。 try块没有引起异常,则执行完try块就执行finally词句。
try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库联接代码时,关闭联接操作等等。
//以Lock加锁为例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
//需要加锁的代码
} finally {
lock.unlock(); //保证锁一定被释放
}
finally遇到如下情况不会执行
try-with-resource
try-with-resource是Java 7中引入的,很容易被忽视。
上面事例中,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
}
}
public final class Scanner implements Iterator, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
try 代码块退出时,会手动调用 scanner.close 方法,和把 scanner.close 方法放到 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的依然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原先的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。
异常基础总结
Java编程思想一书中,对异常的总结。
常用的异常
在Java中提供了一些异常拿来描述时常发生的错误,对于这种异常,有的须要程序员进行捕获处理或申明抛出,有的是由Java虚拟机手动进行捕获处理。Java中常见的异常类:
异常实践
在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也须要耗费好多时间来思索怎么处理异常,包括须要处理什么异常,怎样处理等等。这也是绝大多数开发团队就会制订一些规则来规范进行异常处理的诱因。
当你抛出或捕获异常的时侯,有很多不同的情况须要考虑,而且大部分事情都是为了改善代码的可读性或则 API 的可用性。
异常不仅仅是一个错误控制机制,也是一个通讯媒介。因此,为了和朋友更好的合作,一个团队必须要制订出一个最佳实践和规则,只有这样,团队成员能够理解这种通用概念,同时在工作中使用它。
这里给出几个被好多团队使用的异常处理最佳实践。
只针对不正常的情况才使用异常
异常只应当被用于不正常的条件,它们永远不应当被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检测方法规避的RuntimeException异常不应当通过catch 的方法来处理,比如:NullPointerException,IndexOutOfBoundsException等等。
比如,在解析字符串方式的数字时,可能存在数字格式错误,不得通过catch Exception来实现
if (obj != null) {
//...
}
try {
obj.method();
} catch (NullPointerException e) {
//...
}
主要诱因有三点:
在 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 特性。
与上面几行 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);
}
}
}
}
如果你的资源实现了 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);
}
}
尽量使用标准的异常
代码重用是值得倡导的,这是一条通用规则,异常也不例外。
重用现有的异常有几个益处:
Java标准异常中有几个是常常被使用的异常。如下表格:
异常 使用场合 IllegalArgumentException 参数的值不合适 IllegalStateException 参数的状态不合适 NullPointerException 在null被严禁的情况下参数值为null IndexOutOfBoundsException 下标越界 ConcurrentModificationException 在严禁并发更改的情况下,对象监测到并发更改 UnsupportedOperationException 对象不支持顾客恳求的方式
虽然它们是Java平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。例如,如果你要实现例如复数或则矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是十分合适的。如果一个异常满足你的须要,则不要迟疑,使用就可以,不过你一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须构建在语义的基础上,而不是名子的基础上。
最后,一定要清楚,选择重用哪一种异常并没有必须遵守的规则。例如,考虑扑克对象的情形,假设有一个用于发牌操作的方式,它的参数(handSize)是发一手牌的扑克张数。假设调用者在这个参数中传递的值小于整副牌的剩余张数。那么这些情形既可以被解释为IllegalArgumentException(handSize的值太大),也可以被解释为IllegalStateException(相对顾客的恳求而言,纸牌对象的扑克太少)。
对异常进行文档说明
当在技巧上申明抛出异常时,也须要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避开或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。
/**
* Method description
*
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
同时,在抛出MyBusinessException 异常时,需要尽可能精确地描述问题和相关信息,这样无论是复印到日志中还是在监控工具中,都还能更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
优先捕获最具体的异常
大多数 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)
}
}
不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应当这样做!
如果在 catch 子句中使用 Throwable ,它除了会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应当由应用程序处理的严重问题。 典型的事例是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况导致的,无法处理。
所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下才能处理错误。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
不要忽视异常
很多时侯,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或则记录日志。
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}
但现实是常常会出现难以预想的异常,或者难以确定这儿的代码未来是不是会改动(删除了制止异常抛出的代码),而此时因为异常被捕获,使得难以领到足够的错误信息来定位问题。
合理的做法是起码要记录异常的信息。
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e); // see this line
}
}
不要记录并抛出异常
这可能是本文中最常被忽视的最佳实践。
可以发觉好多代码甚至泛型中就会有捕获异常、记录日志并再度抛出的逻辑。如下:
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
这个处理逻辑看着是合理的。但这常常会给同一个异常输出多条日志。如下:
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
如上所示,后面的日志也没有附加更有用的信息。如果想要提供愈发有用的信息,那么可以将异常包装为自定义异常。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
因此,仅仅当想要处理异常时才去捕获,否则只须要在方式签名中申明让调用者去处理。
包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并才能做针对的异常处理。 在你这样做时,请确保将原始异常设置为诱因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方式,它接受一个 Throwable 作为参数。否则,你将会遗失堆栈跟踪和原始异常的消息,这将会使剖析造成异常的异常风波显得困难。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
不要使用异常控制程序的流程
不应当使用异常控制应用的执行流程,例如,本应当使用if句子进行条件判定的情况下,你却使用异常处理,这是十分不好的习惯,会严重影响应用的性能。
不要在finally块中使用return。
try块中的return句子执行成功后,并不马上返回,而是继续执行finally块中的句子,如果此处存在return句子,则在此直接返回,无情遗弃掉try块中的返回点。
如下是一个例子:
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}
深入理解异常
我们再深入理解下异常,看下底层实现。
JVM处理异常的机制?
提到JVM处理异常的机制,就须要提到Exception Table,以下称为异常表。我们姑且不急于介绍异常表,先看一个简单的 Java 处理异常的小反例。
public static void simpleTryCatch() {
try {
testNPE();
} catch (Exception e) {
e.printStackTrace();
}
}
上面的代码是一个很简单的事例,用来捕获处理一个潜在的空表针异常。
当然若果只是看简简单单的代码,我们很难看出哪些深奥之处,更没有了明天文章要谈论的内容。
所以这儿我们须要利用一把神兵神器,它就是javap,一个拿来拆解class文件的工具,和javac一样由JDK提供。
然后我们使用javap来剖析这段代码(需要先使用javac编译)
//javap -c Main
public static void simpleTryCatch();
Code:
0: invokestatic #3 // Method testNPE:()V
3: goto 11
6: astore_0
7: aload_0
8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
看到里面的代码,应该会有会心一笑,因为总算见到了Exception table,也就是我们要研究的异常表。
异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下
那么异常表用在什么时候呢
答案是异常发生的时侯,当一个异常发生时