异常是什么
异常是 Java 表达“出问题了,我没法照常继续”的方式。运行时不会返回一个错误的值,也不会悄悄弄坏你的数据,而是创建一个描述问题的异常对象,然后把它抛出。正常执行在这一刻停止,Java 开始寻找知道如何处理这种情况的代码。
如果没有代码处理它,异常会一直传到程序的最顶层,JVM 打印出堆栈跟踪,进程以非零状态退出。
注意 "after" 从来没有打印出来。numbers[5] 一被求值,就抛出了 ArrayIndexOutOfBoundsException,main 剩下的部分全部被放弃。访问超过数组最后一个有效索引的位置,是触发异常最常见的方式之一。
阅读堆栈跟踪
当一个异常没有被处理时,你会看到类似下面的输出。它看起来吓人,其实只是一个列表:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:8)
at Main.main(Main.java:4)
从上往下读:
- 第一行是异常的类型(
ArithmeticException)和消息(/ by zero)。 - 每一行
at ...是一个栈帧。最上面的一行是异常真正被抛出的位置:第 8 行的divide。它下面是调用者:第 4 行的main。
最上面那行 at 是你最先要看的地方。它下面的栈帧回答的是“我们是怎么走到这里的?”
运行这段代码,堆栈跟踪会直接指向 a / b 那一行,然后显示 main 是调用者。这条调用链是 Java 免费送给你的最有用的调试工具。
异常的继承体系
每个异常都是一个对象,它们都继承自 Throwable。重要的两个分支是:
Error:JVM 抛出的严重问题,比如OutOfMemoryError或StackOverflowError。一般不要捕获它们。Exception:程序可以合理预见并处理的问题。它下面有RuntimeException,也就是NullPointerException这类日常 bug 的父类。
Throwable
├── Error (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArithmeticException
└── ArrayIndexOutOfBoundsException
因为这些都是真正的类,异常可以携带消息,你也可以向它查询关于它自己的信息:
getMessage() 返回堆栈跟踪中冒号后面的文字;getClass().getSimpleName() 给出异常类型的名字。
受检异常与非受检异常
这是最容易让新手犯迷糊的区别。
- 非受检异常继承
RuntimeException。它们通常意味着你的代码有 bug:一个你没料到的null、错误的索引、除以零。编译器不会强制你处理它们。 - 受检异常继承
Exception但不继承RuntimeException(例如IOException)。它们代表你无法控制的情况:文件不存在、网络连接断开。编译器强制你要么捕获它们,要么声明它们。
如果一个方法可能抛出受检异常,就必须用 throws 声明,而且每个调用者都必须处理它:
把 main 上的 throws Exception 删掉,代码连编译都通不过,这就是编译器在执行受检异常的约定。非受检异常永远不需要这样做。
抛出你自己的异常
你不只是被动应对异常,也可以主动抛出异常。用 throw 加上一个新的异常对象,表示某个参数或状态无效。这比返回 -1 之类的魔法值、然后指望调用者去检查要好得多。
一定要附上一条消息,说明出了什么问题,最好再带上出错的值。将来读堆栈跟踪的你会感谢现在的自己。校验输入时,最常用的两个异常是 IllegalArgumentException 和 IllegalStateException。
常见陷阱
- 吞掉异常。 捕获异常后什么也不做(一个空的代码块)会把 bug 藏起来。至少记一条日志;通常你应该处理它或重新抛出它。
- 捕获范围过大的
Exception。 捕获基类Exception(更糟的是Throwable)可能掩盖你本来不打算处理的问题。只捕获你预期的具体类型。 - 从下往上读堆栈跟踪。 最相关的栈帧是最上面那行
at,而不是最下面。从那里开始看。 - 把
Error和Exception混为一谈。 不要尝试从OutOfMemoryError或StackOverflowError中恢复,而是去修复根本原因。
下一篇:try-catch
现在你已经知道异常是什么,也会阅读它们了。下一篇讲如何真正处理异常:把有风险的代码放进 try 块,在 catch 中恢复,并在 finally 中运行清理代码,让程序继续运行而不是崩溃。
常见问题
Java 中的异常是什么?
异常是一个对象,代表程序运行时发现的问题,比如除以零、索引越过数组末尾,或者在 null 上调用方法。问题出现时,Java 会抛出这个异常:正常流程停止,Java 开始寻找能处理它的代码。如果没有代码处理它,程序会打印堆栈跟踪并退出。
Java 中受检异常和非受检异常有什么区别?
受检异常(Exception 的子类但不是 RuntimeException 的子类,例如 IOException)必须被捕获,或者用 throws 声明,这由编译器强制检查。非受检异常(RuntimeException 的子类,例如 NullPointerException、ArrayIndexOutOfBoundsException)通常意味着程序有 bug,不需要声明。Error(例如 OutOfMemoryError)也属于非受检,一般不应该去捕获。
如何阅读 Java 的堆栈跟踪?
从上往下读。第一行写出异常类型和消息(例如 java.lang.ArithmeticException: / by zero)。下面每一行 at ... 都是一个栈帧,显示方法、文件和行号,从抛出异常的位置开始,沿着调用者一路往回走。最上面那行 at 几乎总是你最先要看的地方。