异常处理的作用
- 向用户通报错误。
- 返回到一个安全的状态,并能执行一些命令。
- 保存所有工作结果,并以妥善的方式退出。
常见错误
- 输入错误
- 设备错误
- 物理限制
- 代码错误
Java 中异常对象都是派生于 Throwable 类的一个实例,如果内置的异常不能满足要求,用户可以创建自己的异常类。
异常结构简化示意图
Error 类层级结构描述了 Java 运行时系统的内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象。
Exception 层次结构又派生为两类:
RuntimeException
: 由程序错误导致的异常- 错误的类型转换
ClassCastException
- 数组访问越界
java.lang.ArrayIndexOutOfBoundsException
- 访问 null 指针
java.lang.NullPointerException
- ……
- 错误的类型转换
- 其他异常: 由于像 I/O 错误这类错误导致的异常
- 试图在文件尾部后面读取数据
IOException
- 试图打开一个不存在的文件
EOFException
- 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在
FileNotFoundException
- ……
- 试图在文件尾部后面读取数据
“如果出现 RuntimeException 异常,那么就一定是你的问题” 是一条相当有规则的道理
递归-堆栈溢出
int a = new int[2000000000]
内存溢出(最简单的内存溢出)
抛出异常
throws
放置到方法头上throw
直接抛出一个异常
出现异常时判断异常类型,创建一个异常对象然后抛出。例:
1 | EOFException e = new EOFException(); |
或
1 | throw new EOFException() |
以下四种情况应当抛出异常
- 调用一个抛出受查异常的方法
- 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常
- 程序出现错误
- Java 虚拟机和运行时库出现的内部异常
如果会出现前两种情况,应当根据异常规范(exception specification),在方法的首部声明方法可能抛出的异常,如果可能抛出多个受查异常,应全部声明,如:
1 | public Image loadImage(String s) throws FileNotFoundException,EOFException{ |
但是 Java 的内部错误(即从 Error 继承的错误)不需要声明,同样也不应该声明从 RuntimeException 继承的非受查异常,这些运行时错误完全在我们的掌控之中。应当花时间修正错误,而不是说明错误发生的可能性。
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制,要么就应该避免发生。
catch 语句抛出异常
1 | try{ |
如上,ServletException 用带有异常信息文本的构造器来构造。但是有另一种更好的将原始异常设为新异常”原因”的方法。
1 | try{ |
当捕获到异常时,就可以使用Throwable e = se.getCause()
重新得到原始异常,这种包装技术可以让用户抛出子系统的高级异常,而不丢失原始异常(Java 核心技术书中推荐)。
捕获异常
1 | try{ |
如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常,那么
- 程序将跳过 try 语句块其他代码
- 程序将执行 catch 子句中的处理器代码
如果没有抛出异常,那么程序将跳过 catch 语句,如果抛出一个没有声明的异常,那么程序将退出。可以使用e.getMessage()
可以获取异常信息(如果有的话),或者使用e.getClass().getName()
获取异常对象的实际类型。try catch
语句可以捕获多个异常,所以应该将异常从子类到父类排序,避免异常类型错误。
注释:捕获多个异常时,异常变量隐含为 final 变量
异常处理
在方法内可以throws exception
而最终调用这个方法的人需要用try catch
解决异常 ,通常应该捕获那么知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。
finally 语句
finally 语句是一定会执行的方法,无论是否抛出异常(无论是否被 catch 语句声明)一共有三种情况:
- 代码没有抛出异常:执行 try 语句,然后执行 finally 语句块。
- 代码抛出一个在 catch 子句捕获的异常:执行 try 语句块中代码,直到报错为止。此刻,程序将跳过 try 语句块中剩余代码,转而去执行于该异常匹配的 catch 语句块中的代码,然后执行 finally 语句块。
- 代码抛出一个未声明的异常:程序将执行 try 语句块中的代码直至有异常抛出,然后跳过 try 剩余代码执行 finally 子句中的语句,并将异常抛给代码的调用者。
建议解耦合try/catch
和try/finally
语句块,提高代码清晰度。
Tips:finally 中的 return 语句将替换之前的 return 语句。
带资源的 try 语句
1 | open a resource |
如果资源属于一个实现了 AutoCloseable 接口的类,Java SE 7 提供了一种快捷方式void close() throws Exception
1 | try(Scanner in = new Scanner(new FileInputStream("/user/words"),"UTF-8"); |
如上,这个块可以调用多个资源,同时在正常退出或者存在一个异常时,都会调用in.close()
方法,跟使用 finally 模块一样。
使用异常机制的技巧
- 异常处理不能代替简单测试:异常处理更慢
- 不要过分细化异常:提高清晰度,降低代码量
- 利用异常层次结构
- 不要压制异常
- 在检测错误时,“苛刻”比放任好(优先选择抛出异常)
- 不要羞于传递异常
Tips:5,6 可以归纳为“早抛出,晚收获”。
断言
在编写代码时,我们总是会做出一些假设,如果使用判断语句来验证假设,验证语句将会一直保留在程序中,即使测试完毕也不会自动删除。如果程序中存在大量的这种判断,会严重影响的程序执行速度。
断言(Assertion)是一种调试程序的方式。在 Java 中,使用 assert 关键字来实现断言。当代码发布时,assert 语句将会自动地被移走。
assert 语句有两种形式:
assert 条件;
assert 条件 : 表达式;
这两种形式都会对条件进行检测,如果结果为 false,则抛出一个AssertionError
异常。在第二种形式中,表达式将传入AssertionError
的构造器,并转换成一个消息字符串。
例如:
1 | public static void main(String[] args) { |
语句assert x >= 0;
即为断言,断言条件x >= 0
预期为true
。如果计算结果为false
,则断言失败,抛出AssertionError
。
1 | assert x >= 0 : "x must >= 0"; |
如果如上带上消息,断言失败的时候,AssertionError 会带上消息x must >= 0
,更加便于调试。
参考
- Java 核心技术
- 廖雪峰的官方网站