我们在写程序的时候经常会出现许多意想不到的错误,在 Java 中,错误也是被包装成了各种子类实例。只要我们能捕捉到包装错误的对象,就能做出对应的处理方式。
语法与继承架构
使用try,catch
Java 中的所有错误都会被打包成对象。我们可以尝试(try)捕捉(catch)代表错误的对象,然后做一些处理。例如下面的代码,我们将输入几个数字并打印它的平均值,Scanner 对象的 nextInt() 方法会默认输入的下一个字符串代表数字,如果我们输入了 3o 就会出现错误:
import java.util.*;
public class average {
public static void main(String[] args) {
try {
Scanner console = new Scanner(System.in);
double sum = 0;
int count = 0;
while(true) {
int number = console.nextInt();
if(number == 0) {
break;
}
sum += number;
count++;
}
System.out.printf("平均 %.2f\n", sum/count);
} catch(InputMismatchException) {
System.out.println("必须输入整数");
}
}
}
使用了 try,catch 语法的程序代码,JVM会尝试执行 try 区块中的程序代码。如果发生错误的话,执行流程会跳离错误发生点,然后会将抛出的错误类型与catch括号中声明的类型进行比对,如果相对应,就会执行catch区块中的程序代码。
我们也可以在捕捉处理错误之后,尝试恢复程序正常的执行流程,只需要将上面的 catch 中代码稍加修改并且加入到 while 循环中即可:
catch(InputMismatchException) {
System.out.printf("略过非整数输入: %s\n", console.next());
}
异常继承架构
在 Java 中,所有的错误都被包装为对象,这些对象都是可抛出的,它们都继承自 java.lang.Throwable 类,Throwable 定义了取得错误信息,堆栈追踪等方法,它有两个子类: java.lang.Error 与 java.lang.Exception。
Error 被称为非受检异常,因为它代表的是严重的系统错误,如硬件错误或 JVM 内存不足等错误,这是 Java 应用程序没办法处理的,所以当出现这种错误时,我们基本不用处理,任其传播。
一般来说,如果某个方法会抛出 Throwable 或其子类实例,只要不是 Error, java.lang.RuntimeException 或其子类实例,你就必须使用 try,catch 语法加以处理,或者 throws 声明这个方法会抛出异常。否则会编译失败。这种错误一般被称为受检异常。
如果父类异常对象在子类异常对象之前被捕捉,那么 catch 子类异常对象的区块永远也不会被执行。解决这个问题的方法一般是改变异常对象捕捉的顺序。
如果有时catch区块都在做相同的事情,在JDK7开始,我们可以使用多重捕捉语法:
try {
做一些事情...
} catch(IOException | InterruptedException | ClassCastException) {
.....
}
我们在括号中列出来的异常不得有继承关系,否则会发生编译错误。
要抓还是要抛
为什么我们会有这样的疑问,假设当我们在设计时没有充足的信息知道该对异常进行哪种处理的话,那么我们就不该抓(处理)这个异常。我们应该抛出异常,让调用方法的客户端来处理,我们所处的位置是比客户端要低的,所以我们不知道的信息他们有可能知道,所以我们可以将异常抛给上层来让它们处理,但是我们必须明确一点的是,既然会把异常抛给上层,那么就表示我们认为调用方法的客户端是有能力且应该处理异常的。
实际上在异常发生时,我们可以先处理我们能够处理的部分,剩下的无法处理的部分,可以抛出给调用方法的客户端处理。当我们处理完能处理的异常之后,还要将没办法处理的异常进行抛出,这时,我们可以用 throw(注意没有s)来进行抛出。我们可以在任何流程中抛出异常,不一定非要在 catch 中,如果抛出的是受检异常,我们必须在方法上使用 throws 声明。
使用继承时,父类中的某个方法被声明为 throws 某些异常,子类在重新定义该方法时可以:
- 不声明 throws 任何异常;
- throws 父类该方法中声明的某些异常;
- throws 父类该方法中声明异常的子类。
认识堆栈追踪
当我们调用多个方法时发生异常,我们可以使用堆栈追踪来得知异常发生的根源,这是利用异常对象自动收集的。查看堆栈追踪最简单的方法就是直接调用异常对象的 printStackTrace() 方法。
堆栈追踪信息中显示了异常的类型,最顶层是异常的根源,以下是调用的调用方法的顺序。
如果想要取得个别的堆栈追踪元素进行处理,可以使用 getStackTrace(),这会返回 StackTraceElement 数组,数组的 0 索引为异常根源的相关信息,之后为各方法调用的信息,如 StackTraceElement 的 getClassName() 等方法取得相应的信息。
在使用 throw 重抛异常的时候,异常的追踪堆栈起点,仍是异常的发生根源,而不是重抛异常的地方。如果想要让异常堆栈起点为重抛异常的地方,可以使用 fillInStackTrace() 方法,这个方法会重新装填异常堆栈,将起点设为重抛异常的地方,并返回 Throwable 对象,来看个例子:
public class StackTraceDemo {
public static void main(String[] args) {
try{
c();
} catch(NullPointerException ex) {
ex.printStackTrace();
}
}
static void c() {
try {
b();
} catch(NullPointerException ex) {
ex.printStackTrace();
Throwable t = ex.fillInStackTrace();
throw(NullPointerException ex) t;
}
}
static void b() {
a();
}
static String a() {
String text = NULL;
return text.toUpperCase();
}
}
注意:在阿里 Java 开发手册中,有这样一条强制性规则:Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过 catch NumberFormatException 来实现。
阿里的工程师们给出的建议是,对于可以通过预检方式规避的 RuntimeException 异常,我们不应该进行捕获,但是我们在前面也提到过了,对于所有的非受检异常,我们都不应该进行捕获,在 oracle 的官方文档中,也明确说明了:
RuntimeException and its subclasses are unchecked exceptions. Unchecked exceptions do not need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.
关于这一点所产生的分歧,有可能是我的理解还不够所造成的。但在这里,我还是建议大家遵守阿里的 Java 开发手册会更加保险一些。
关于assert
关于断言(assert),我们知道在设计一个程序的时候,当程序执行的某个时间点或某个情况下,一定是处于或不处于何种状态,我们可以断言,此时可以使用 assert 关键字。
assert的使用方法有两种:
- assert boolean_expression;
- assert boolean_expression : detail_expression。
对于上面两种方法,当 boolean_expression 为 true,则什么事情都不会发生,如果为 false,则发生 java.lang.AssertionError,此时若采取第二个语法,则将会 detail_expression 的结果显示出来,如果当中是个对象,则调用 toString() 显示文字描述结果。
异常与资源管理
使用finally
使用 finally 关键字,当我们撰写 try,catch 区块,无论 try 区块中有无异常发生,只要撰写有 finally 区块,则 finally 区块一定会被执行。
如果程序撰写的流程中先 return 了,而且也有 finally 区块,那么 finally 会先被执行,然后才会将值返回。
自动尝试关闭资源
当我们想要尝试自动关闭资源一个对象的时候,是可以撰写在 try 之后的括号之中的。如果我们打开了一个文件,当读写完毕后要自动关闭文件,这时就可以使用这个语法:
try(Scanner console = new Scanner(new FileInputStream(name)))
它会自动调用 console 的 close 方法,关闭 Scanner 和 FileInputStream 的相关资源。这是 JDK7 新增的语法(try-with-resources)。也是一个程序语法蜜糖,如果有兴趣的同学可以尝试反编译一下,看看其中的原理。
java.lang.AutoCloseable
自定义尝试自动关闭资源语法的对象,必须操作 java.lang.AutoCloseable 接口,它是 JDK 新增的语法,仅定义了 close 方法。
public interface AutoCloseable {
void close() throws Exception;
}
只要操作这个接口,就可以套用尝试关闭资源语法。看个例子:
public class AutoCloseableDemo {
public static void main(String[] args) {
try(Resource res = new Resource()) {
res.doSome();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
class Resource implements AutoCloseable {
void dosome() {
System.out.println("做一些事");
}
@Override
public void close() throws Exception {
System.out.println("资源被关闭");
}
}
尝试关闭资源语法也可以同时关闭两个以上的对象资源,只要中间以“;”隔开,并且在 try 括号中越后面撰写的对象资源会越早被关闭。如果想知道为什么,你可以尝试进行反编译来看一下程序代码。