文章目录
- 1. Error 和 Exception 区别是什么?
- 运行时异常和一般异常(受检异常)区别是什么?
- JVM 是如何处理异常的?
- throw 和 throws 的区别是什么?
- final、finally、finalize 有什么区别?
- NoClassDefFoundError 和 ClassNotFoundException 区别?
- try-catch-finally 中哪个部分可以省略?
- try-catch-finally 中,如果 catch 中 return 了, finally 还会执行吗?
- 类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA。
- Java异常处理最佳实践
- 使用 finally 代码块
- Java 7 的 try-with-resource 语法
- 优先明确的异常
- 对异常进行文档说明
- 使用描述性消息抛出异常
- 优先捕获最具体的异常
- 不要捕获 Throwable 类
- 不要忽略异常
- 不要记录并抛出异常
- 包装异常时不要抛弃原始的异常
- 不要使用异常控制程序的流程
- 使用标准异常
- 异常会影响性能
- 异常处理-阿里巴巴Java开发手册
1. Error 和 Exception 区别是什么?
Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出 等,编译器不会对这类错
误进行检测,JAVA 应用程序也不应对这类错误进行捕 获,一旦这类错误发生,通常应用程序会被终
止,仅靠应用程序本身无法恢复;
Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错 误,应对其进行处理,使
应用程序可以继续正常运行。
运行时异常和一般异常(受检异常)区别是什么?
运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出 现的异常。 Java 编译器
不会检查运行时异常。
受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编 译器会检查受检异常。
RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处 理此异常,如果强制要求
调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,
如果没有特殊的要求,我们建 议使用RuntimeException异常。
JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM, 该异常对象包含异常名
称,异常描述以及异常发生时应用程序的状态。创建异常 对象并转交给 JVM 的过程称为抛出异常。可能
有一系列的方法调用,终才进 入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常 处理代码。当 JVM 发现可
以处理异常的代码时,会把发生的异常传递给它。如 果 JVM 没有找到可以处理该异常的代码块,JVM 就
会将该异常转交给默认的异 常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息
并 终止应用程序。
throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出 异常,可以通过 throws
关键字在方法上声明该方法要拋出的异常,或者在方法 内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中 的异常,受查异常
和非受查异常都可以被抛出。 - throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出 的异常列表。一个
方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中 必须包含可处理异常的代码,否
则也要在方法签名中用 throws 关键字声明相应的异 常。
final、finally、finalize 有什么区别?
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方 法不能被重写、修
饰变量表示该变量是一个常量不能被重新赋值。 - finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执 行的代码方法放在
finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用 来存放一些关闭资源的代码。 - finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类, Java 中允许使用
finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的 清理工作。
NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该 尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类 的定义,该动作发生在
运行期间,即编译时该类存在,但是在运行时却找不到 了,可能是编译后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其 进行捕获和处理,或在方
法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或
ClassLoader.findSystemClass 动 态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会
抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中, 另一个加载器
又尝试去加载它。
try-catch-finally 中哪个部分可以省略?
答:catch 可以省略
更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时 异常+普通异常。也就是
说,如果你只用try去处理普通异常(受检异常)却不加以catch处 理,编译是通不过的,因为编译器硬性规定,普通
异常(受检异常)如果选择捕获,则必须用 catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,
所以 catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对 所有代码加上try,代码
在运行期时也只不过是在正常运行的基础上加一层皮。 但是你一旦对一段代码加上try,就等于显示地承
诺编译器,对这段代码可能抛 出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须
用 catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾 处理,或者加上catch
捕获以便进一步处理。
至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
如下
try-catch-finally 中,如果 catch 中 return 了, finally 还会执行吗?
答:会执行,在 return 前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块, try中的 return 语句不会
立马返回调用者,而是记录下返回值待 finally 代码块 执行完毕之后再向调用者返回其值,然后如果在
finally 中修改了返回值,就会 返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很
大的 困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也 可以通过提升编译
器的语法检查级别来产生警告或错误。
类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA。
有如下代码片断:
1 try {
2 throw new ExampleB("b")
3 } catch(ExampleA e){
4 System.out.println("ExampleA");
5 } catch(Exception e){
6 System.out.println("Exception");
7 }
请问执行此段代码的输出是什么?
答:
输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类 型],抓取 ExampleA 类
型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常)
面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书)
1 class Annoyance extends Exception {
2 }
3 class Sneeze extends Annoyance {
4 }
5 class Human {
6 public static void main(String[] args)
7 throws Exception {
8 try {
9 try {
10 throw new Sneeze();
11 } catch ( Annoyance a ) {
12 System.out.println("Caught Annoyance");
13 throw a;
14 }
15 } catch ( Sneeze s ) {
16 System.out.println("Caught Sneeze");
17 return ;
18 } finally {
19 System.out.println("Hello World!");
20 }
21 }
22 }
结果
1 Caught Annoyance
2 Caught Sneeze
3 Hello World!
Java异常处理最佳实践
在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花
费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都
会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个
被很多团队使用的异常处理 佳实践。
- 在 finally 块中清理资源或者使用 try-with-resource 语句
当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在 try块的最后关闭资源。
1 public void doNotCloseResourceInTry() {
2 FileInputStream inputStream = null;
3 try {
4 File file = new File("./tmp.txt");
5 inputStream = new FileInputStream(file);
6 // use the inputStream to read a file
7 // do NOT do this
8 inputStream.close();
9 } catch (FileNotFoundException e) {
10 log.error(e);
11 } catch (IOException e) {
12 log.error(e);
13 }
14 }
问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资
源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而
且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的 后部分。结果就是,你
并没有关闭资源。
所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。
使用 finally 代码块
与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功
执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资
源。
1 public void closeResourceInFinally() {
2 FileInputStream inputStream = null;
3 try {
4 File file = new File("./tmp.txt");
5 inputStream = new FileInputStream(file);
6 // use the inputStream to read a file
7 } catch (FileNotFoundException e) {
8 log.error(e);
9 } finally {
10 if (inputStream != null) {
11 try {
12 inputStream.close();
13 } catch (IOException e) {
14 log.error(e);
15 }
16 }
17 }
18 }
Java 7 的 try-with-resource 语法
如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这
个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
1 public void automaticallyCloseResource() {
2 File file = new File("./tmp.txt");
3 try (FileInputStream inputStream = new FileInputStream(file);) {
4 // use the inputStream to read a file
5 } catch (FileNotFoundException e) {
6 log.error(e);
7 } catch (IOException e) {
8 log.error(e);
9 }
10 }
优先明确的异常
你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异
常。
因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的
处理异常并且避免额外的检查。因此,总是尝试寻找 适合你的异常事件的类,例如,抛出一个
NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。
对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可
以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景
使用描述性消息抛出异常
在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,
都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需
要用一到两句话描述即可。
如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信
息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被
java.lang.Long 类的构造函数抛出。
优先捕获最具体的异常
大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问
的代码块。
但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获
IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch
块,因为它是 IllegalArgumentException 的子类。总是优先捕获最具体的异常类,并将不太具体的
catch 块添加到列表的末尾。你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个
catch 块处理所有 NumberFormatException 异常,第二个处理所有非
NumberFormatException 异常的IllegalArgumentException 异常
1 public void catchMostSpecificExceptionFirst() {
2 try {
3 doSomething("A message");
4 } catch (NumberFormatException e) {
5 log.error(e);
6 } catch (IllegalArgumentException e) {
7 log.error(e)
8 }
9 }
不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你
永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,
指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError。
两者都是由应用程序控制之外的情况引起的,无法处理。
所以, 好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。
不要忽略异常
很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。
1 public void doNotIgnoreExceptions() {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 // this will never happen
6 }
7 }
但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出
的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录
异常的信息。
1 public void logAnException() {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 log.error("This should never happen: " + e);
6 }
7 }
不要记录并抛出异常
这可能是本文中 常被忽略的 佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次
抛出的逻辑。如下:
1 try {
2 new Long("xyz");
3 } catch (NumberFormatException e) {
4 log.error(e);
5 throw e;
6 }
这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:
1 17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatExcep tion:
For input string: "xyz"
2 Exception in thread "main" java.lang.NumberFormatException: For input str ing:
"xyz"
3 at java.lang.NumberFormatException.forInputString(NumberFormatException.j
ava:65)
4 at java.lang.Long.parseLong(Long.java:589)
5 at java.lang.Long.(Long.java:965)
6 at com.stackify.example.TestExceptionHandling.logAndThrowException(TestEx
ceptionHandling.java:63)
7 at
com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:5
8)
如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装
为自定义异常。
1 public void wrapException(String input) throws MyBusinessException {
3 // do something
4 } catch (NumberFormatException e) {
5 throw new MyBusinessException("A message that describes the error.", e);
6 }
7 }
包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。
在你这样做时,请确保将原始异常设置为原因(注:参考下方代码NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。
1 public void wrapException(String input) throws MyBusinessException {
2 try {
3 // do something
4 } catch (NumberFormatException e) {
5 throw new MyBusinessException("A message that describes the error.", e);
6 }
7 }
不要使用异常控制程序的流程
不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。
使用标准异常
如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
异常会影响性能
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
仅在异常情况下使用异常;在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。
异常处理-阿里巴巴Java开发手册
- 【强制】Java 类库中定义的可以通过预检查方式规避的
RuntimeException异常不应该通过catch 的方式来处理,比如:
NullPointerException,IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比
如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException
来实现。 正例:if (obj != null) {…} 反例:try { obj.method(); } catch
(NullPointerException e) {…} - 【强制】异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意
外情况,且异常的处理效率比条件判断方式要低很多。 - 【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于
非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。 - 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该
异常抛给它的调用者。 外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 - 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事
务。 - 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。 说明:如果JDK7
及以上,可以使用try-with-resources方式。 - 【强制】不要在finally块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,
而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中
的返回点。 反例:
1 private int x = 0;
2 public int checkReturn() {
3 try {
4 // x等于1,此处不返回
5 return ++x;
6 } finally {
7 // 返回的结果是2
8 return ++x;
9 }
10 }
- 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对
方抛的是绣球,实际接到的是铅球,就会产生意外情况。 - 【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进
行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什
么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。 - 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什
么情况下会返回null值。 说明:本手册明确防止NPE(空指针异常)是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。 - 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景: 1) 返回类型为基本数据类型,
return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:public int f() { return Integer对
象}, 如果为null,自动解箱抛NPE。 2) 数据库的查询结果可能为null。 3) 集合里的元素即使
isNotEmpty,取出的数据元素也可能为null。 4) 远程调用返回对象时,一律要求进行空指针判
断,防止NPE。 5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。 6) 级联调用
obj.getA().getB().getC();一连串调用,易产生NPE。
正例:使用JDK8的Optional类来防止NPE问题。 - 【推荐】定义时区分unchecked / checked 异常,避免直接抛出new
RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。 - 【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间
RPC调用优先考虑使用Result方式,封装 isSuccess()方法、“错误码”、“错误简短信息”。 说明:关
于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就
会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error
message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数
据序列化和传输的性能损耗也是问题。 - 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代
码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共
性方法,或者抽象公共类,甚至是组件化。 正例:一个类中有多个public方法,都需要进行数行相
同的参数校验操作,这个时候请抽取: private boolean checkParam(DTO dto) {…}