1.异常介绍

Java异常是Java提供的一种识别及响应错误的一致性机制。 Java的异常机制可以使程序中异常处理代码与正常业务代码分离,保证程序代码更加优雅,并提高程序的健壮性。在有效使用异常的情况下,异常能够清晰的回答what、where、why这三个问题:异常类型清晰回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。

2.Java异常架构

3.4.异常体系 - 图1

Java中分为异常和错误两大类,他们有一个共同的超类Throwable,Throwable包含了其线程创建时线程执行的快照,它提供了printStackTrace()等接口用于获取堆栈追踪数据等信息。

  • Error(错误)

    一般表示程序中无法处理的错误,这个错误一般出现在代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、OutOfMemoryError:内存不足错误、StackOverflowError:栈溢出错误等,这一类错误发生后,JVM将终止线程。错误是不受检查异常,非代码性错误。按照java惯例,我们不应该实现Error的子类,应用程序不应该去处理错误。

  • Exception(异常)

    异常是程序本身可以捕捉,并且可以处理的。他本身又分为两大类:运行时异常和编译时异常

    • 运行时异常

      运行时异常表示的是JVM在运行期间可能出现的异常。Java编译器不会检查它,当程序中可能出现这类异常时,如果没有通过throws声明抛出,也没有用try-catch语句捕捉,他是能够编译通过的,比如NullPointerException(空指针)异常、ArrayIndexOutBoundException(数组下标越界异常)、ClassCastException(类型转换异常)、 ArithmeticExecption(算术异常)等。这类异常属于不受检查异常,一般是程序的错误逻辑引起的,在程序中可以选择捕获也可以不处理。

    • 编译时异常

      编译时异常表示的是java在编译期会检查它,如果程序中出现此类异常,比如ClassNotFOundException(没有找到指定类异常),IOException(IO流异常),那么必须通过throws或者try-catch进行捕捉处理,否则编译不通过。

  • 受检异常与非受检异常

    Java的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。

    • 受检异常

      受检异常是编译器要求必须处理的异常。一旦发生此类异常,就必须采用某种方式进行处理。除RuntimeException及其子类外,其他的Exception异常都属于受检异常。编译器会检查此类异常,也就是说,当编译器检查到应用程序某处可能会此类异常时候,将会提示必须try-catch或者在方法签名中使用throws关键字抛出,否则编译不通过。

    • 非受检异常

      非受检异常就是编译器不会进行检查并且不要求必须处理的异常,也就是说,当程序中出现此类异常时,即使我们没有try-catch捕捉它,也没有使用throws抛出,编译会正常通过。这类异常包含运行时异常和错误。

3.Java异常关键字

  • try-用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw – 用于抛出异常。
  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

    4.常见异常处理方式

    4.1.直接抛出

    不知道怎么处理的异常,那么将异常传递下去,交给调用该方法的地方处理,在方法签名上使用throws关键字声明抛出的异常。

  1. private static void readFile(String filePath)throws IOException {
  2. File file =newFile(filePath);
  3. String result;
  4. BufferedReader reader =new BufferedReader(newFileReader(file));
  5. while((result = reader.readLine())!=null){
  6. System.out.println(result);
  7. }
  8. reader.close();
  9. }

4.2.封装异常再抛出

多系统集成时,当某个子系统发生故障时候,异常类型可能有很多种,可以使用统一的异常类型向外暴漏,不需要暴漏太多内部异常细节。

  1. private static void readFile(String filePath)throws MyException {
  2. File file =newFile(filePath);
  3. String result;
  4. BufferedReader reader =new BufferedReader(newFileReader(file));
  5. while((result = reader.readLine())!=null){
  6. System.out.println(result);
  7. }
  8. reader.close();
  9. }

4.3.捕捉异常

使用try-catch捕捉异常,并对不同的异常进行不同的处理

4.4.自定义异常

习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用)

  1. public class MyException extends Exception{
  2. public MyException(){
  3. }
  4. public MyException(String msg) {
  5. super(msg);
  6. }
  7. }

5.释放资源的方法

5.1.try-catch-finally

当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和catch语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以 解决这个问题。

5.2.try-with-resource

JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了AutoCloseable 接口的类。

  1. private static void tryWithResourceTest(){
  2. try(Scanner scanner =new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
  3. // code
  4. }catch(IOException e){
  5. }
  6. }

try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。

6.Error 和 Exception 区别是什么?

  • Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终 止,仅靠应用程序本身无法恢复;

  • Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理, 使应用程序可以继续正常运行。

    7.运行时异常和一般异常(受检异常)区别是什么?

  • 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。

  • 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。 RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。

    8.JVM 是如何处理异常的?

    在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发 现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块, JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

    9.throw 和 throws 的区别是什么?

    Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。

  • throw关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。

  • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用throws关键字声明相应的异常。

    10.final、finally、finalize 有什么区别?

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。

  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

    11.NoClassDefFoundError和ClassNotFoundException区别?

    NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是编译后被删除了等原因导致; ClassNotFoundException 是一个受检异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就 会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加 载器又尝试去加载它。

12.try-catch-finally 中哪个部分可以省略?

catch 可以省略

原因 更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普 通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。 理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代 码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。

13.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

会执行,在 return前执行。

注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。

  1. public static intgetInt(){
  2. int a =10;
  3. try{
  4. System.out.println(a /0);
  5. a =20;
  6. }catch(ArithmeticException e){
  7. a =30;
  8. return a;
  9. }finally{
  10. a =40;
  11. }
  12. return a;
  13. }
  14. // 结果:30
  1. public static intgetInt(){
  2. int a =10;
  3. try{
  4. System.out.println(a /0);
  5. a =20;
  6. }catch(ArithmeticException e){
  7. a =30;
  8. return a;
  9. }finally{
  10. a =40;
  11. return a;
  12. }
  13. }
  14. // 结果:40

14.类 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. try {
  8. try {
  9. throw new Sneeze();
  10. } catch (Annoyance a) {
  11. System.out.println("Caught Annoyance");
  12. throw a;
  13. }
  14. } catch (Sneeze s) {
  15. System.out.println("Caught Sneeze");
  16. return;
  17. } finally {
  18. System.out.println("Hello World!");
  19. }
  20. }
  21. }
  22. // 输出:Caught Annoyance、Caught Sneeze、Hello World!
  23. 解释:11行抛出的异常是Sneeze类型,但是用了他的父类Annoyance进行捕获(里氏代换原则),由于他本质还是Sneeze类型,所以,16行仍然能够进行捕获

15.常见的 RuntimeException 有哪些?

  • ClassCastException(类转换异常)
  • IndexOutOfBoundsException(数组越界)
  • NullPointerException(空指针)
  • ArrayStoreException(数据存储异常,操作数组时类型不一致)
  • 还有IO操作的BufferOverflowException异常

    16.Java常见异常有哪些

  • java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。

  • java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
  • java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
  • java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
  • java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
  • java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而 在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
  • java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。 java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于 等于数组大小时抛出。
  • java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于 序列大小时,抛出该异常。
  • java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例, 而该类是一个抽象类或接口时,抛出该异常。
  • java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
  • java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异 常。
  • java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw 语句抛出null等等。
  • java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型, 而该字符串确不满足数字类型要求的格式时,抛出该异常。
  • java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。

    17.异常使用注意事项

  1. 在 finally 块中清理资源或者使用 try-with-resource 语句

    finally代码块总会执行,所以可以在finally块中关闭某些资源连接。java1.7之后提供了 try-with-resource用来自动关闭资源,但是前提是需要关闭资源的类实现了AutoCloseable接口

  2. 优先明确的异常

你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。
因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好 的处理异常并且避免额外的检查。
因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一 个 IllegalArgumentException 。避免抛出一个不明确的异常。

  1. 对异常进行文档说明

当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。

  1. 使用描述性消息抛出异常

在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。 但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。 如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信 息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。

  1. 优先捕获最具体的异常

大多数 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. }
  1. 不要捕获 Throwable 类

Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做! 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。 所以,最好不要捕获Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。

  1. 不要忽略异常

忽略异常可能会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。 合理的做法是至少要记录异常的信息。

  1. 不要记录并抛出异常

    出现很多多余日志

  2. 包装异常时不要抛弃原始的异常

捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。
在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的 原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则, 你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。

  1. public void wrapException(String input)throws MyBusinessException {
  2. try{
  3. }catch(NumberFormatException e){
  4. throw new MyBusinessException("A message that describes the error.", e);
  5. }
  6. }
  1. 不要使用异常控制程序的流程

不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。

  1. 使用标准异常

如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码

  1. 异常会影响性能

异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出 一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。

  • 仅在异常情况下使用异常;
  • 在可恢复的异常情况下使用异常;

尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需 要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。