java之异常机制
异常
什么是异常?异常是指程序运行过程中出现的错误,一旦错误产生就会创建异常对象,我们需要处理这些异常对象。
异常的体系结构
java中,异常对象均派生自Throwable
类,java 异常的体系结构如下
Throwable
有两个子类,分别为为 Error
(错误) 和 Exception
(异常)
Error
: 表示程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。当此类错误发生时,应用不应该去处理此类错误。常见的错误有:Virtual MachineError
(虚拟机运行错误)、NoClassDefFoundError
(类定义错误)、OutOfMemoryError
。此类错误发生时,JVM将终止线程。非代码性错误。Exception
:是程序本身可以捕获并且可以处理的异常。其中可分为运行时异常(RuntimeException)
和非运行时异常
,也叫做受检异常运行时异常(不受检异常)
:RuntimeException
类及其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException
)、数组下标越界(ArrayIndexOutBoundException
)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。非运行时异常(受检异常)
:Exception
中除RuntimeException
及其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException
,必须对该异常进行处理,要么使用try-catch
捕获,要么使用throws
语句抛出,否则编译不通过。
1 | ---> Throwable <--- |
可查异常和不可查异常
- 可查异常: 指 编译器要求必须处置的异常,除了
RuntimeException
及其子类外的所有Exception
及其子类均为可查异常。 - 不可查异常: 指编译器不要求强制处置的异常,包括运行时异常(RuntimeException与其子类)和错误(Error)
如何处理异常
来看示例:
1 |
异常关键字
try
: 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。catch
: 用于捕获异常。catch用来捕获try语句块中发生的异常。finally
: finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。throw
: 用于抛出异常。throws
: 用在方法签名中,用于声明该方法可能抛出的异常。
异常声明 throws
在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。
1 | public static void method() throws IOException, FileNotFoundException{ |
若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
1 | private static void readFile(String filePath) throws IOException { |
Throws
抛出异常的规则:
Throws
必须声明方法可抛出的任何可查异常(checked exception
)。即如果一个方法可能出现可查异常,要么用try-catch
语句捕获,要么用throws
子句声明将它抛出,否则会导致编译错误仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。调用方法必须遵循任何可查异常的处理和声明规则。- 如果是不可查异常(
unchecked exception
),即Error、RuntimeException
或它们的子类,那么可以不使用throws
关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。 - 若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
异常抛出 throw
不合适的代码会导致异常,需要抛出这些异常,使程序正常工作:
1 | public static double method(int value) { |
大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。有时我们会从 catch
中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
1 | private static void readFile(String filePath) throws MyException { |
自定义异常
如果想要自定义异常,继承自``即可
定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如上面用到的自定义MyException。
1 | public class MyException extends Exception { |
捕获异常
捕获异常的方式有如下几种:
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
try-catch
1 | public int getPlayerScore(String playerFile) { |
try-catch-finally
如果在异常出现后,仍需要处理部分逻辑,比如关闭文件等,则需要将最终需要处理的操作放在finally
语句块中。
1 | public int getPlayerScore(String playerFile) |
上述示例中,在向上抛出FileNotFoundException
异常前,会调用finally
语句块的内容,以关闭文件流。
通常情况下会使用如下写法,以防止关闭文件流出错:
1 | public int getPlayerScore(String playerFile) { |
JAVA 7 之后可以自动释放实现了AutoCloseable
接口的资源占用。
1 | public int getPlayerScore(String playerFile) { |
上述代码可以自动释放的原因是 Scanner
实现了Closeable
,而Closeable
继承自AutoCloseable
:
1 | public final class Scanner implements Iterator<String>, Closeable { |
try
代码块退出时,会自动调用 scanner.close
方法,和把 scanner.close
方法放在 finally
代码块中不同的是,若 scanner.close
抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed
方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed
方法来获取。
异常实践
当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。
如下是异常使用的场景
只针对不正常的情况才使用异常
异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检查方式规避的
RuntimeException
异常不应该通过catch
的方式来处理,比如:NullPointerException
,IndexOutOfBoundsException
等等。
比如,在解析字符串形式的数字时,可能存在数字格式错误,不得通过catch Exception
来实现:
1 | try { |
主要原因有如下三点:
- 异常机制的设计初衷是用于不正常的情况,很少会会JVM实现试图对它们的性能进行优化。所以创建、抛出和捕获异常的开销是很昂贵的。
- 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化。
- 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。
在 finally 块中清理资源或者使用 try-with-resource 语句
防止编码时忘记关闭资源等。
尽量使用标准的异常
异常 | 使用场景 |
---|---|
IllegalArgumentException | 参数的值不合适 |
IllegalStateException | 参数的状态不合适 |
NullPointerException | 在null被禁止的情况下参数值为null |
IndexOutOfBoundsException | 下标越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,对象检测到并发修改 |
UnsupportedOperationException | 对象不支持客户请求的方法 |
优先捕获最具体的异常
总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。
1 | public void catchMostSpecificExceptionFirst() { |
不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!
如果在 catch
子句中使用 Throwable
,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError
或者 StackOverflowError
。两者都是由应用程序控制之外的情况引起的,无法处理。所以,最好不要捕获 Throwable
,除非你确定自己处于一种特殊的情况下能够处理错误。
1 | public void doNotCatchThrowable() { |
不要忽略异常
catch语句块里至少要加上日志。
1 | public void logAnException() { |
抛出异常则不要打日志
逻辑上没有问题,但是会给同一个异常输出多条日志
1 | try { |
如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。
1 | public void wrapException(String input) throws MyBusinessException { |
想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理
包装异常时不要抛弃原始的异常
Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。
不要在finally块中使用return
try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。
1 | private int x = 0; |
深入理解异常
引用
1. java-exceptions
2. Java异常详解——一篇文章搞定Java异常
3. Java 基础 - 异常机制详解