异常机制是 Java 提供的一种识别及响应错误的一致性机制。Java 异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅并提高程序健壮性。
Throwable
Throwable 是 Java 语言中所有错误与异常的超类,有 Error(错误)和 Exception(异常)两个子类,它内部包含了其线程创建时线程执行堆栈的快照,并且提供了 printStackTrace() 等方法用于获取堆栈信息。
Error
Error 类及其子类表示程序无法处理的错误,比如 VirtualMachineError、NoClassDefFoundError、OutOfMemoryError、StackOverflowError 等。此类错误发生时 JVM 将终止线程
Exception
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。运行时异常为 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。比如 NullPointerException、ArrayIndexOutBoundException、ClassCastException、ArithmeticExecption 等。此类异常属于非受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
编译时异常为 Exception 中除 RuntimeException 及其子类之外的异常,JVM 会检查这类异常,如果程序中出现此类异常,比如 ClassNotFoundException、IOException等,要么通过 throws 抛出,要么通过 try-catch 捕获处理,否则不能通过编译。程序通常不会自定义该类异常,而是直接使用系统提供的异常类。
异常处理机制
Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适的异常处理器来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。
Java 的异常处理通过 try、catch、throw、throws 和 finally 这 5 个关键词来实现。通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。注意:非受检异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。
一个方法出现编译时异常,就需要 try-catch/throws 处理,否则会导致编译错误。如果觉得解决不了某些异常问题,且不需要调用者处理,那么可以抛出异常。throw 关键字作用是在方法内部抛出一个 Throwable 类型的异常。任何 Java 代码都可以通过 throw 语句来抛出异常。
异常相关知识点
1. Exception 和 Error 的区别?
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出或者被捕获,它是异常处理机制的基本组成类型。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
从性能角度来审视一下 Java 的异常处理机制,这里有两个可能会相对昂贵的地方:
try-catch 代码段会产生额外的性能开销,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁那这个开销就不能被忽略了。
2. throw 和 throws 的区别?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。throws 关键字和 throw 关键字在使用上的几点区别如下:
throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
3. NoClassDefFoundError 和 ClassNotFoundException 的区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是编译后被删除了等原因导致的。
ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获或者在方法签名中使用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存时,如果通过传入的类路径参数没有找到该类就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
4. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
答:会在 return 前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回给调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或修改返回值会对程序造成很大的困扰。
代码示例1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
输出结果:30
因为在return之前,要返回的信息已经算好了,此时在finally中改变a的值不会对已经算好的信息产生影响
代码示例2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
输出结果:40
异常处理最佳实践
在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。本文给出几个被很多团队使用的异常处理最佳实践。
1. 在 finally 块中清理资源或使用 try-with-resource
当使用类似 InputStream 这种需要使用后关闭的资源时,一个常见的错误就是在 try 块的最后关闭资源。
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(new File("./tmp.txt"));
......
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
这段代码的问题在于,当没有异常抛出时,这段代码可以正常工作,并且资源也可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是并没有关闭资源。所以,应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。
使用 finally 代码块
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);
}
}
}
}
但是如果使用传统的 try-cache-finally 语句,当 finally 块中的异常没有捕获而抛出时,异常信息会覆盖 try 块中的异常信息。
使用 try-with-resource 语法
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
......
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
使用这种语法,try 和 finally 中的异常信息都可以得到保留。
2. 使用描述性消息抛出异常
在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。如果抛出一个特定的异常,它的类名很可能已经描述了这种错误,此时就不需要提供很多额外的信息。
一个很好的例子是 NumberFormatException 。当以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
3. 不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。可以在 catch 子句中使用它,但是永远不应该这样做!如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。
典型的例子是 OutOfMemoryError 或 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。所以最好不要捕获 Throwable ,除非确定自己处于一种特殊的情况下能够处理错误。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
4. 不要记录并抛出异常
可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:
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);
}
}
5. 包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。在这样做时,请确保不要抛弃原始异常信息。
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);
}
}
6. 使用标准异常
如果使用 JDK 内置的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足要求,这时候才考虑去创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
7. 异常会影响性能
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗 1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。
异常使用规范
综上所述,当抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念并在工作中使用它。
异常处理-阿里巴巴Java开发手册
【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等。无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException来实现。
【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。对大段代码进行 try-catch 不利于定位问题,也是一种不负责任的表现。
【强制】在调用RPC、二方包或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截。通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。
【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景:1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。2) 数据库的查询结果可能为 null。3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess() 方法、“错误码”、“错误简短信息”。关于 RPC 方法返回方式使用 Result 方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。