Java8基础知识(六)异常

异常

所有的异常都是由Throwable继承而来的子类,分为ErrorException两种:

  1. Error类描述了Java运行时系统的内部错误和资源耗尽错误,应将这种异常通告给用户并尽力使程序安全地终止。
  2. Exception分为RuntimeExceptionIOException两种:
    • RuntimeException描述由于程序错误导致的异常,例如:错误的类型转换数组访问越界访问空指针等等。
    • IOException描述非程序错误导致的异常(通常是用户输入错误导致的),例如:试图在文件尾部之后读取数据试图打开一个不存在的文件试图根据给定的字符串查找类对象,而字符串表示的类不存在等等。

ErrorRuntimeException称为非受查异常,而IOException称为受查异常

声明受查异常

一个方法必须声明所有可能抛出的受查异常。而非受查异常或为不可控制的Error,或为应由程序避免发生的RuntimeException,不应当抛出。抛出异常的规范如下:

class SomeClass {
    // some fields
    // ...
    // some methods
    // ...
    // method which may throw exception
    public void method() throws someExcpetion {
        // some code
        // ...
    }
}

注意:若子类覆盖了超类的某个方法,则子类中该方法不可以声明比超类更通用的异常。若超类方法没有抛出任何受查异常,则子类方法也不能抛出受查异常。

抛出异常

当一个方法声明了某个异常时,应当在异常出现时将其抛出。

class someClass {
    public void method throws someException {
        if (exception statements) {
            throw new someException();
        }
    }
}

如果存在合适的异常来描述异常情况,就可以直接使用这个异常类。否则,需要自己定义异常来满足需求。

习惯上,定义的异常类应包含两个基础的构造器:默认构造器和带有详细描述信息的构造器。Throwable类的toString方法会打印这些详细信息,便于调试。

class FileFormatException extends IOException {
    public FileFormatException() {}
    public FileFormatException(String gripe) {
        super(gripe);
    }
}

捕获异常

当调用了一个抛出受查异常的方法时,必须对其进行处理,使程序能够捕获其抛出的异常或继续向外抛出该异常。

try {
    // method throws exception
    // ...
}catch(Exception e) {
    // do something
    // ...
}

注意:若超类的方法没有抛出异常,覆盖的方法也不能抛出异常,则必须捕获覆盖方法中可能出现的每一个受查异常。

异常链

当方法捕获异常而再次抛出时,应该将其包装为新的高级异常,并注意保留原始异常的细节。

以数据库异常为例:

try {
    // access the database
}catch(SQLException e) {
    Throwable se = new ServletException("database error");
    se.initCause(e);
    throw se;
}
// 捕获se时可以通过Throwable e = se.getCause()来得到原始异常

finally子句

显然try代码块与catch代码块在尾部经常包含重叠的代码。例如:资源回收处理等。可以将这些代码转移到finally子句中,这样无论是否捕获异常,这些代码都能很好地执行,且不需要重复编写。

可以通过解耦合的方式提高try/catch/finally语句块的清晰度,同时能报告finally子句中出现的错误。

InputStream in = ...;
try {
    try {
        // code that might throw exceptions
    }
    finally {
        in.close();
    }
}catch(Exception e) {
	// show error message
}

注意:finally子句会在return 语句前执行,所以不应当在finally子句中使用return语句。

try语句涉及资源(实现了AutoCloseable接口的类)时,try块退出或出现异常时需要在finally子句中调用AutoCloseableclose方法。但这个方法声明了异常,处理起来就会比较困难。使用带资源的try语句能很好地解决这一问题。

try(Resource res = ...) {
    // work with res
}

通过上述方法编写try块,当try块正常退出或出现异常时,都会自动调用close方法,不需要使用嵌套。

否则若try块和close方法均发生异常,后者就会被自动捕获,由addSuppressed方法添加到前者中。需要调用getSuppressed方法得到从close方法抛出的被抑制的异常列表才能对这些异常进行观察,十分麻烦。

分析堆栈轨迹元素

堆栈轨迹是一个方法调用过程的列表,记录了方法调用的顺序。

可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();

更灵活的方法是使用getStackTrace方法,得到堆栈轨迹元素的数组。

Throwable t= new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames) {
    // analyze frame
    // ...
}

更全面地,可以使用静态方法Thread.getAllStackTrace来产生所有线程的堆栈轨迹。

Map<Thread StackTraceElement[]> map = Thread.getAllStackTrace();
for (Thread t : map.keySet()) {
    StackTraceElement[] frames = map.get(t);
    // analyze frames
    // ...
}

以递归阶乘方法为例:

package exception;

import java.util.Scanner;

public class StackTraceTest {

    public static int factorial(int n) {
        System.out.println("factorial(" + n + "):");
        Throwable t = new Throwable();
        StackTraceElement[] frames = t.getStackTrace();
        for(StackTraceElement frame : frames) {
            System.out.println(frame);
        }
        int r;
        if(n <= 1) r = 1;
        else r = n * factorial(n-1);
        System.out.println("return " + r);
        return r;
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("Enter n: ");
        int n = in.nextInt();
        factorial(n);
    }
}

输入结果如下:

factorial(3):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.main(StackTraceTest.java:25)
factorial(2):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.main(StackTraceTest.java:25)
factorial(1):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.main(StackTraceTest.java:25)
return 1
return 2
return 6

异常机制技巧

  1. 不应用异常处理代替简单的测试,会造成性能的浪费。
  2. 不应过分细化异常,会导致代码量膨胀,难以排除错误。
  3. 应使用最合适的异常类型,积极使用子类异常和自定义类异常。
  4. 不应压制异常,可能导致更加严重的错误。
  5. 应当尽早抛出存在的异常。
  6. 应灵活地包装并传递异常。
posted @ 2020-03-28 15:12  Aries99C  阅读(299)  评论(0)    收藏  举报