Java基础(014):Java的异常结构

  本篇主要就Java异常结构和相关注意点等方面进行介绍和说明,目录结构如下:

 

1、Java的异常层次结构

  来自《The Java™ Tutorials》[1]中关于异常的定义说明:


The term exception is shorthand for the phrase "exceptional event."

Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions.


  简单地说,异常表示程序的非正常执行流程这种意外事件。

  Java 的异常被设计用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的;然而,它对于发现某些编译器无法检测到的编程错误,也是非常重要的。[5]

  Java 中的异常都源自顶层父类 Throwable ,所有异常类都直接或间接继承自 Throwable 类,Throwable 有2个直接子类 Error 和 Exception ,而 Exception 下还有属于 Unchecked exception 且继承 RuntimeException 的运行时异常和除 RuntimeException 外其他直接继承 Exception 的 Checked Exception。总的来说,Java的异常结构主要包括以下2部分(参考 [4]11.1.1. The Kinds of Exceptions 的说明):

  • Unchecked exception :包括 RuntimeException 、 Error 及他们的子类
  • Checked Exception :除了 RuntimeException/Error 外的其他继承 Exception(准确来说是 Throwable ) 的子类

  这就是 Java 异常的主要结构,简要展示如下:

 

2、Error 和 Exception 的区别

  • 错误 Error :表示系统级(JVM等)的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,应用程序很难去处理这种情况,比如内存溢出,不可能指望程序能处理这样的情况。Error 用来表示编译时和系统错误(除特殊情况外,一般不用你关心)
    • 内存不够,字节码不合法等
  • 异常 Exception :表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,则不会发生的情况。Exception 是可以被抛出的基本类型,在 Java 类库、用户方法以及运行时出现故障中都可能抛出 Exception 型异常

 

3、CheckedException 和 RuntimeException 的区别

  异常 Exception 包括有2种:属于不受检异常 Unchecked exception 的运行时异常 RuntimeException(及其子类) 和除了 RuntimeException 外的直接继承 Exception 的 Checked Exception(也称为编译时异常)。

 

3.1、RuntimeException

  运行时异常 RuntimeException 是属于不受检异常/非受检异常的一部分[Java 8 API docs]:


RuntimeException is the superclass of those exceptions that can be thrown during the normal operation of the Java Virtual Machine.

RuntimeException and its subclasses are unchecked exceptions. Unchecked exceptions do not need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.


  这里说的运行时,或者不受检,应该是对于编译器来说的,编译期不用检查,即不会强制检查代码中是否显式处理了,默认情况下, RuntimeException 会得到JVM自动处理,所以通常可以不用捕获 RuntimeException 。也即是所说的不受检 Unchecked,不用主动特地去检查。

  属于运行时异常的类型有很多,它们被java自动抛出,所以不必在异常说明中把它们列出来。非常方便的是,通过将这些异常设置为 RuntimeException 的子类而把它们归类起来,这是继承的一个绝佳例子:建立具有相同特征和行为的一组类型。[5]

  RuntimeException 代表的是编程错误[5]:

  • 无法预料的错误。比如从你控制范围之外传递进来的 null 引用引发的 NullPointerException。
  • 作为程序员,应该在代码中进行检查的错误,主要是代码逻辑问题,应该主动去修正和避免(比如对于 ArrayIndexOutOfBoundsException,就得注意一下数组的大小了)。在一个地方发生的异常,常常会在另一个地方导致错误。

  在这些情况下使用异常很有好处,它们能给调试带来便利。如果不捕获这种类型的异常会发生什么事呢?因为编译器没有在这个问题上对异常说明进行强制检查, RuntimeException 类型的异常也许会穿越所有的执行路径直达外层调用方法(例如 main 方法),而不会被捕获。

  代码中只有 RuntimeException(及其子类)类型的异常可以被忽略, 因为编译器强制要求处理所有受检查类型的异常。

 

3.2、Checked Exception

  关键词:受检异常、编译时异常

  除了 RuntimeException 和 Error 及其子类以外的异常,都属于 Checked Exception ,这些是编译时被强制检查的异常,因此称为受检异常。Java编译器要求程序必须通过 try-catch 捕获或通过方法声明抛出(throws)这种异常。一个方法必须通过 throws 语句在方法的声明部分说明它可能抛出但并未捕获的所有 Checked Exception 。

 

3.3、CheckedException 和 RuntimeException 的区别

  异常表示程序运行过程中可能出现的非正常状态,运行时异常(RuntimeException,属于不受检异常 Unchecked exception)表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。Java 编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常(虚拟机自动抛出运行时异常)。

  受检异常(Checked Exception)适用于那些不是因程序引起的错误情况,这类异常是由一些外部的偶然因素所引起的,比如:读取文件时文件不存在引发的 FileNotFoundException 。而不受检异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException。

 

3.4、运行时异常与一般异常有何异同(同上)

  注:这里的运行时异常指的是 RuntimeException 及其子类,一般异常指的是除了 RuntimeException 外的其他继承 Exception 的异常子类(即 CheckedException )。

 

3.5、常见的运行时异常 RuntimeException

java.lang.NullPointerException
java.lang.NumberFormatException
java.lang.ArithmeticException
java.lang.IndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.ClassCastException
java.util.ConcurrentModificationException
java.lang.UnsupportedOperationException
java.lang.IllegalArgumentException
java.lang.IllegalStateException

  

3.6、常见的编译时异常 Checked Exception 

java.lang.ClassNotFoundException
java.lang.CloneNotSupportedException
java.lang.IllegalAccessException
java.lang.InterruptedException
java.lang.NoSuchFieldException
java.lang.NoSuchMetodException

  

4、Java 异常的一些使用建议

  异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在《Effective Java》中对异常的使用给出了以下指导原则:

  • 只针对异常情况才使用异常,不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
  • 优先使用标准的异常
  • 每个方法抛出的异常都要有文档
  • 保持失败的原子性
  • 不要在catch中忽略掉捕获到的异常

  《On Java 8》 给的建议,应该在下列情况下使用异常:

  • 尽可能使用 try-with-resource。
    • 或者在 finally 块中手动关闭资源
  • 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
  • 解决问题并且重新调用产生异常的方法。
  • 进行少许修补,然后绕过异常发生的地方继续执行。
  • 用别的数据进行计算,以代替方法预计会返回的值。
  • 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
  • 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  • 终止程序。
  • 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
  • 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)

  其他一些开发实践建议:

  • 重新抛出的异常,尽量包含引起异常的完整堆栈信息(异常链),或者相关的详细描述信息(message)
  • 通过 Logger 记录相关的异常堆栈信息日志,以备排查。
  • 异常实例的构造代价高昂,可以通过错误码+提示信息进行替代且同时可以排查到原因的,就尽量不使用异常
    • 在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

 

5、finally 关键字

  当要把除内存之外的资源恢复到它们的初始状态,且无论 try 中是否出现异常都需要执行时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。[5]

  finally 的相关问题:

  • finally 中抛出异常会覆盖原来的异常
  • finally 和 return 的执行顺序
    • finally 中使用 return 会直接返回而使得即使发生了异常,也会像没有异常输出一样
    • finally 中使用 return 返回值会直接使用该返回值,忽略前面的返回值
  • finally 不会执行的几种场景
    • try-catch 中使用 System.exit(其他类似) 会直接退出,不会再执行 finally 中的语句。
    • finally 中使用 System.exit(其他类似) 会直接退出。

  

6、关键字 throw 和 throws

  • throws :用在方法声明后面,表示方法可能抛出的异常类型,提示方法使用者在使用该方法时需要注意/捕获的异常类型。注意是可能抛出,并不一定会。
  • throw :用在方法内部,表示主动抛出某个具体的异常实例,且对于受检异常需要是 throws 声明的异常类型或其子类型。

 

7、参考

 

posted @ 2021-04-05 16:48  心明谭  阅读(187)  评论(0编辑  收藏  举报