异常

什么是异常?异常是指程序运行过程中出现的错误,一旦错误产生就会创建异常对象,我们需要处理这些异常对象。

异常的体系结构

java中,异常对象均派生自Throwable类,java 异常的体系结构如下
java异常体系结构

Throwable有两个子类,分别为为 Error(错误) 和 Exception(异常)

  1. Error: 表示程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。当此类错误发生时,应用不应该去处理此类错误。常见的错误有:Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、OutOfMemoryError。此类错误发生时,JVM将终止线程。非代码性错误。
  2. Exception:是程序本身可以捕获并且可以处理的异常。其中可分为运行时异常(RuntimeException)非运行时异常,也叫做受检异常
    • 运行时异常(不受检异常)RuntimeException类及其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
    • 非运行时异常(受检异常)Exception中除RuntimeException及其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说 IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
1
2
3
4
5
6
7
8
9
              ---> Throwable <--- 
| (checked) |
| |
| |
---> Exception Error
| (checked) (unchecked)
|
RuntimeException
(unchecked)

可查异常和不可查异常

  • 可查异常: 指 编译器要求必须处置的异常,除了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
2
3
public static void method() throws IOException, FileNotFoundException{
//something statements
}

若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。

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

Throws抛出异常的规则:

  • Throws必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。调用方法必须遵循任何可查异常的处理和声明规则。
  • 如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
  • 若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
异常抛出 throw

不合适的代码会导致异常,需要抛出这些异常,使程序正常工作:

1
2
3
4
5
6
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出 运行时异常
}
return 1.0 / value;
}

大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。

1
2
3
4
5
6
7
8
9
private static void readFile(String filePath) throws MyException {    
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
自定义异常

如果想要自定义异常,继承自``即可
定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如上面用到的自定义MyException。

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

捕获异常的方式有如下几种:

  • try-catch
  • try-catch-finally
  • try-finally
  • try-with-resource
try-catch
1
2
3
4
5
6
7
8
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile) {
throw new IllegalArgumentException("File not found");
}
}
try-catch-finally

如果在异常出现后,仍需要处理部分逻辑,比如关闭文件等,则需要将最终需要处理的操作放在finally语句块中。

1
2
3
4
5
6
7
8
9
10
11
12
public int getPlayerScore(String playerFile)
throws FileNotFoundException {
Scanner contents = null;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} finally {
if (contents != null) {
contents.close();
}
}
}

上述示例中,在向上抛出FileNotFoundException异常前,会调用finally语句块的内容,以关闭文件流。
通常情况下会使用如下写法,以防止关闭文件流出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int getPlayerScore(String playerFile) {
Scanner contents;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
} finally {
try {
if (contents != null) {
contents.close();
}
} catch (IOException io) {
logger.error("Couldn't close the reader!", io);
}
}
}

JAVA 7 之后可以自动释放实现了AutoCloseable 接口的资源占用。

1
2
3
4
5
6
7
8
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e ) {
logger.warn("File not found, resetting score.");
return 0;
}
}

上述代码可以自动释放的原因是 Scanner实现了Closeable,而Closeable继承自AutoCloseable

1
2
3
4
5
6
public final class Scanner implements Iterator<String>, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}

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

异常实践

当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。

如下是异常使用的场景

只针对不正常的情况才使用异常

异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch的方式来处理,比如:NullPointerExceptionIndexOutOfBoundsException等等。

比如,在解析字符串形式的数字时,可能存在数字格式错误,不得通过catch Exception来实现:

1
2
3
4
5
try { 
obj.method();
} catch (NullPointerException e) {
//...
}

主要原因有如下三点:

  • 异常机制的设计初衷是用于不正常的情况,很少会会JVM实现试图对它们的性能进行优化。所以创建、抛出和捕获异常的开销是很昂贵的。
  • 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化。
  • 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。
在 finally 块中清理资源或者使用 try-with-resource 语句

防止编码时忘记关闭资源等。

尽量使用标准的异常
异常 使用场景
IllegalArgumentException 参数的值不合适
IllegalStateException 参数的状态不合适
NullPointerException 在null被禁止的情况下参数值为null
IndexOutOfBoundsException 下标越界
ConcurrentModificationException 在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException 对象不支持客户请求的方法
优先捕获最具体的异常

总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。

1
2
3
4
5
6
7
8
9
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
不要捕获 Throwable 类

Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!

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

1
2
3
4
5
6
7
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
不要忽略异常

catch语句块里至少要加上日志。

1
2
3
4
5
6
7
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e); // see this line
}
}
抛出异常则不要打日志

逻辑上没有问题,但是会给同一个异常输出多条日志

1
2
3
4
5
6
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}

如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。

1
2
3
4
5
6
7
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}

想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理

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

Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。

不要在finally块中使用return

try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。

1
2
3
4
5
6
7
8
9
10
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}

深入理解异常

引用

1. java-exceptions
2. Java异常详解——一篇文章搞定Java异常
3. Java 基础 - 异常机制详解