异常处理的作用

  • 向用户通报错误。
  • 返回到一个安全的状态,并能执行一些命令。
  • 保存所有工作结果,并以妥善的方式退出。

常见错误

  • 输入错误
  • 设备错误
  • 物理限制
  • 代码错误

Java 中异常对象都是派生于 Throwable 类的一个实例,如果内置的异常不能满足要求,用户可以创建自己的异常类。

异常结构简化示意图

20200807223425

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
2
EOFException e = new EOFException();
throw e;

1
throw new EOFException()

以下四种情况应当抛出异常

  1. 调用一个抛出受查异常的方法
  2. 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常
  3. 程序出现错误
  4. Java 虚拟机和运行时库出现的内部异常

如果会出现前两种情况,应当根据异常规范(exception specification),在方法的首部声明方法可能抛出的异常,如果可能抛出多个受查异常,应全部声明,如:

1
2
3
public Image loadImage(String s) throws FileNotFoundException,EOFException{

}

但是 Java 的内部错误(即从 Error 继承的错误)不需要声明,同样也不应该声明从 RuntimeException 继承的非受查异常,这些运行时错误完全在我们的掌控之中。应当花时间修正错误,而不是说明错误发生的可能性。

总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制,要么就应该避免发生。

catch 语句抛出异常

1
2
3
4
5
try{

}catch (SQLException e){
throw new ServletException("database error: " + e.getMessage());
}

如上,ServletException 用带有异常信息文本的构造器来构造。但是有另一种更好的将原始异常设为新异常”原因”的方法。

1
2
3
4
5
6
7
try{

}catch(SQLException e){
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}

当捕获到异常时,就可以使用Throwable e = se.getCause()重新得到原始异常,这种包装技术可以让用户抛出子系统的高级异常,而不丢失原始异常(Java 核心技术书中推荐)。

捕获异常

1
2
3
4
5
6
try{
code
more code
}catch (ExceptionType e){
handle for this type
}

如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常,那么

  • 程序将跳过 try 语句块其他代码
  • 程序将执行 catch 子句中的处理器代码

如果没有抛出异常,那么程序将跳过 catch 语句,如果抛出一个没有声明的异常,那么程序将退出。可以使用e.getMessage()可以获取异常信息(如果有的话),或者使用e.getClass().getName()获取异常对象的实际类型。try catch语句可以捕获多个异常,所以应该将异常从子类到父类排序,避免异常类型错误。
注释:捕获多个异常时,异常变量隐含为 final 变量

异常处理

在方法内可以throws exception 而最终调用这个方法的人需要用try catch解决异常 ,通常应该捕获那么知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。

finally 语句

finally 语句是一定会执行的方法,无论是否抛出异常(无论是否被 catch 语句声明)一共有三种情况:

  1. 代码没有抛出异常:执行 try 语句,然后执行 finally 语句块。
  2. 代码抛出一个在 catch 子句捕获的异常:执行 try 语句块中代码,直到报错为止。此刻,程序将跳过 try 语句块中剩余代码,转而去执行于该异常匹配的 catch 语句块中的代码,然后执行 finally 语句块。
  3. 代码抛出一个未声明的异常:程序将执行 try 语句块中的代码直至有异常抛出,然后跳过 try 剩余代码执行 finally 子句中的语句,并将异常抛给代码的调用者。

建议解耦合try/catchtry/finally语句块,提高代码清晰度。

Tips:finally 中的 return 语句将替换之前的 return 语句。

带资源的 try 语句

1
2
3
4
5
6
open a resource
try{
work with the resource
}finally{
close the resource
}

如果资源属于一个实现了 AutoCloseable 接口的类,Java SE 7 提供了一种快捷方式
void close() throws Exception

1
2
3
4
5
6
try(Scanner in = new Scanner(new FileInputStream("/user/words"),"UTF-8");
PrintWriter out = new PrintWriter("out.txt")){
while(in.hasNext()){
out.println(in.next().toUpperCase());
}
}

如上,这个块可以调用多个资源,同时在正常退出或者存在一个异常时,都会调用in.close()方法,跟使用 finally 模块一样。

使用异常机制的技巧

  1. 异常处理不能代替简单测试:异常处理更慢
  2. 不要过分细化异常:提高清晰度,降低代码量
  3. 利用异常层次结构
  4. 不要压制异常
  5. 在检测错误时,“苛刻”比放任好(优先选择抛出异常)
  6. 不要羞于传递异常

Tips:5,6 可以归纳为“早抛出,晚收获”。

断言

在编写代码时,我们总是会做出一些假设,如果使用判断语句来验证假设,验证语句将会一直保留在程序中,即使测试完毕也不会自动删除。如果程序中存在大量的这种判断,会严重影响的程序执行速度。

断言(Assertion)是一种调试程序的方式。在 Java 中,使用 assert 关键字来实现断言。当代码发布时,assert 语句将会自动地被移走。

assert 语句有两种形式:

  1. assert 条件;
  2. assert 条件 : 表达式;

这两种形式都会对条件进行检测,如果结果为 false,则抛出一个AssertionError异常。在第二种形式中,表达式将传入AssertionError的构造器,并转换成一个消息字符串。

例如:

1
2
3
4
5
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
}

语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError

1
assert x >= 0 : "x must >= 0";

如果如上带上消息,断言失败的时候,AssertionError 会带上消息x must >= 0,更加便于调试。

参考

多线程