Java核心技术 异常、断言和日志

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


 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值