1.处理异常
如果由于出现错误而使得某些操作没有完成,程序应该:
1.返回到一种安全状态,并能够让用户执行一些其他命令
2.允许用户保存所有操作的结果,并以妥善方式终止程序。
可能出现的错误和问题:
1.用户输入错误
2.设备错误
3.物理限制
4.代码错误
对于方法中的一个错误,传统的做法是返回一个特殊的错误码。
异常处理机制在代码无法执行后,会搜索能够处理这种异常状况的异常处理器(exception handler)。
异常分类
Java中,异常对象都是派生于Throwable类的一个实例。如果Java中内置的异常类不能够满足需求,可以创建自己的异常类。
所有异常都是由Throwable继承而来,但在下层立即分为:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这种内部错误,除了通告给用户,并尽力使程序安全地终止之外,也无能为力。
Exception层次,分解为两个分支:一个派生于RuntimeException,另一个是其他异常。划分的规则是:由程序错误导致的异常属于RuntimeException:
1.错误类型转换
2.数组访问越界
3.访问null指针
而程序本身没问题,但由于想I/O错误这里问题导致的异常属于其他异常:
1.试图在文件尾部后面读取数据
2.试图打开一个不存在的文件
3.试图根据给定的字符串查找Class对象,而这个类不存在
如果出现RuntimeException异常,那么就一定是你的问题。
Java将派生于Error类和RuntimeException类的所有异常成为非受检查(unchecked)异常,所有其他异常被称为受查(checked)异常。
声明受查异常
遇到下面4中情况时应该抛出异常:
1.调用一个抛出受查异常的方法
2.程序运行过程中出现错误,并且利用throw语句抛出一个受查异常
3.程序出现错误,抛出一个非受查异常
4.java虚拟机和运行时库出现错误
如果可能抛出多个受查异常类型,就必须在方法首部列出所有异常类。每个类用逗号隔开:
public Image loadImage(String s) throws FileNotFoundException, EOFException{
...
}
不需要声明Error继承的Java内部错误,任何程序都有抛出这些异常的潜能,而我们对其没有任何控制能力。同样也不应该声明RuntimeException继承的那些非受查异常。
如果子类中覆盖了超类的一个方法,子类方法声明的受查异常不能比超类中声明的异常更通用(子类可以抛出更特定的异常,或者根本不抛出异常)。
如何抛出异常
String readDate(Scanner in) throws EOFException {
...
while () {
if (!in.hasNext()) {
if (n < len) {
throw new EOFException();
}
}
}
return s;
}
EOFException类还有一个含有一个字符串型参数的构造器。 这个构造器可以更加细致的描述异常出现的情况。
String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);
创建异常类
程序中可能会遇到任何标准异常都没有能够充分地描述清楚的问题,那需要做的只是定义一个派生于Exception的类,或者派生于Exception子类的类。
习惯上,定义的类应该包含两个构造器,一个是默认构造器,另一个是带有详细说明的构造器:
public class FileFormatException extends IOException {
public FileFormatException() {
}
public FileFormatException(String message) {
super(message);
}
}
2.捕获异常
捕获异常
try(...) catch(ExceptionType e) {...}
如果在try语句块中的任何代码抛出一个catch子句中说明的异常类:
1.程序将跳过try语句块的其他代码
2.程序将执行catch子句中的处理器代码
如果try语句中没有任何异常抛出,那么程序将跳过cath子句。
如果try语句抛出了catch中没有声明的异常类型,那么方法就会立刻退出。
public void read(String filename) {
try {
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1) {
// 输入
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
read方法有可能抛出一个IOException异常,这种情况下,将跳出整个while循环,进入catch子句,并生成一个栈轨迹。
通常,最好的选择是什么也不做,而是将异常传递给调用者:
public void read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1) {
// 输入
}
}
执行throw说明符,如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。
捕获多个异常
try{
}catch(FileNotFoundException e){
}carch(UnknownHostException e){
}catch(IOException e){}
异常对象可能包含与异常本身有关的信息,想获得对象的更多信息,可以使用e.getMessage(),或者使用e.getClass().getName()得到异常对象的事实类型。
在Java SE 7中,同一个catch子句中可以捕获多个异常类型:
try {
} catch (FileNotFoundException | UnknownHostException e) {
} catch (IOException exception) {
}
只有当不活的异常类型彼此之间不存在子类关系时才需要这个特性。
再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。
try {
}catch (SQLException e) {
throw new ServletException("database error:" + e.getMessage());
}
ServletException用带有异常信息文本的构造器来构造。
可以有一种更好的处理方法,并且将原始异常设置为新异常的原因:
try {
} catch (SQLException e) {
Throwable se = new ServletException("databse error");
se.initCause(e);
throw se;
}
当捕获异常时,就可以使用下面语句重新得到原始异常:
Throwable e = se.getCasue();
这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
有时可能只想记录一个异常,再将它重新抛出,而不做任何改变:
try {
} catch (Exception e) {
logger.log(level, message, e);
throw e;
}
finally子句
回收资源问题:
一种解决方案是捕获并重新抛出所有的异常。这种方案比较乏味,这是因为需要在两个地方清除所分配的资源。一个在正常的代码中;另一个在异常代码中。
Java有一种更好的解决方案,就是finally子句。
不管是否有异常被捕获,finally子句中的代码都被执行。
InputStream in = new FileInputStream(...);
try {
// 1
会出现异常的地方
// 2
} catch (IOException e) {
// 3
展示错误信息
// 4
} finally {
// 5
in.close();
}
// 6
3种情况会执行finally子句:
1.代码没有抛出异常。1,2,5,6
2.抛出一个在catch子句中捕获的异常。如果catch子句没有抛出异常,执行1,3,4,5,6。如果抛出异常执行1,3,5。
3.代码catch子句抛出了一个异常,但这个异常不是由catch子句捕获的。这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里执行1,5处的语句。
try语句可以只有finally子句,而没有catch子句:
InputStream in = new FileInputStream(...);
try {
} finally {
in.close();
}
无论在try语句块中是否遇到异常,finally子句中的in.close()语句都会被执行。
这里建议解耦合try/catch和try/finally语句块,可以提高代码的清晰度。
InputStream in = ...;
try {
try {
} finally {
in.close();
}
} catch (IOException e) {
}
内存的try语句块确保关闭输入流。外层的try语句块确保报告出现的错误。这样还有一个好处,就是将会报告finally子句中出现的错误。
当finally子句包含return语句时,这个返回值将会覆盖原始的返回值。
InputStream in = new FileInputStream(...);
try {
} finally {
in.close();
}
如果在try中抛出一个非IOException,执行finally,并调用close方法。而close本身可能抛出IOException,这种情况下原始的异常将会丢失,转而抛出close方法的异常。
带资源的try语句
假设资源属于一个实现了AutoCloseable接口的类,Java SE 7为上诉问题提供了有用的快捷方式。AutoCloseable接口有一个方法:
void close() throws Exception
带资源的try语句(try-with-resources)的最简形式:
try (Resource res = ...) {
使用资源
}
try块退出时,会自动调用res.close()。
try (Scanner in = new Scanner(new FileInputStream("E://test.txt"), "UTF-8")) {
while (in.hasNext()) {
System.out.println(in.next());
}
}
这个块正常退出时或者存在一个异常时,都会调用in.close()方法。还可以指定多个资源:
try (Scanner in = new Scanner(new FileInputStream("E://test.txt"), "UTF-8");
PrintWriter out = new PrintWriter("out.text")) {
while (in.hasNext()) {
out.println(in.next().toUpperCase());
}
}
带资源的try语句会将原来的异常重新抛出,而close方法抛出的异常会被抑制。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。可以通过getSuppressed方法,得到从close方法抛出并被抑制的异常列表。
分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
使用getStackTrace方法,会得到StackTraceElement对象的一个数组:
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames) {
}
StackTraceElement类含有能够获得文件名和当前执行代码行号的方法,同时能够获得类名和方法名,toString方法将产生一个格式化的字符串,其中包含所获得的信息。
静态的Thread.getAllStackTrace,可以产生所有线程的堆栈轨迹:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet()) {
StackTraceElement[] frames = map.get(t);
}
public class StackTraceTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("输入 n:" );
int n = in.nextInt();
factorial(n);
}
public static int factorial(int n) {
System.out.println("阶层(" + n + "):");
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement f : frames) {
System.out.println(f);
}
int r;
if (n <= 1) {
r = 1;
} else {
r = n * factorial(n - 1);
}
System.out.println("返回 " + r);
return r;
}
}
使用异常机制的技巧
1.异常处理不能代替简单的测试
2.不要过分地细化异常
3.利用异常层次结构
4.不要压制异常
5.在检测错误时,苛刻要比放任更好
6.不要羞于传递异常
7.在try后面catch不能先大后小,不能先IOException,后FileNotFoundException,因FileNotFoundException包括在IoException中