Java中的异常体系

基础鸭😝😝😝

Posted by 华仔 on February 28, 2019

Java中的异常分类继承关系

java异常体系结构

Error和Exception

Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

运行时异常和非运行时异常

​ 运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 ​ 非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

异常的捕获和处理

try{
    //可能出现异常
}catch(Throwable1 t1) {
    //处理异常
}catch(Throwable2 t2) {
    //处理异常
}finally{
    //始终执行的代码块
}

异常捕获时候仅仅try代码块为必须的,其余的可以选用,例如可以:try catch或try finally或try catch finally。并且catch代码块可以有多个,但是必须大范围catch在下,例如try catch(NullPointerException e) catch(Exception e)…以此类推。

try-with-resources

在JDK1.7时,提供了try-with-resources,用于方便释放io资源或其他。

public class Demo {
    public static void main(String[] args) {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    if (bout != null) {
                        try {
                            bout.close();
                        }
                        catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

上述代码使用一个输出流bin和一个输入六bout,将一个文件中的数据写入另一个文件。由于IO资源非常宝贵,因此在完成操作后,必须在finally中分别释放这两个资源。并且为了能够正确释放这两个IO资源,需要用两个finally代码块嵌套的方式完成资源的释放。

在上述40行代码中,真正处理IO操作的代码不到10行,而其余30行代码都是用于保证资源合理释放的。这显然导致代码可读性较差。不过好在JDK 1.7提供了try-with-resources解决这一问题。修改后的代码如下:

public class TryWithResource {
    public static void main(String[] args) {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我们需要将资源声明代码放入try后的括号中,然后将资源处理代码放入try后的{}中,catch代码块中仍然进行异常处理,并且无需写finally代码块。

那么,try-with-resources为什么能够避免大量资源释放代码呢?答案是,由Java编译器来帮我们添加finally代码块。注意,编译器只会添加finally代码块,而资源释放的过程需要资源提供者提供。

在JDK 1.7中,所有的IO类都实现了AutoCloseable接口,并且需要实现其中的close()函数,资源释放过程需要在该函数中完成。

那么,编译器在编译时,会自动添加finally代码块,并将close()函数中的资源释放代码加入finally代码块中。从而提高代码可读性。

异常屏蔽

如果在try catch finally中均有异常抛出,那么最终只会抛出finally中的异常

public class TestThrowable {
    public static void main(String[] args) {
        try {
            throw new RuntimeException("finally中抛出异常");
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("catch中抛出异常");
        } finally {
            throw new RuntimeException("finally中抛出异常");
        }
    }
}

执行结果:

java.lang.RuntimeException: finally中抛出异常
	at cn.huazai.main.throwable_example.TestThrowable.main(TestThrowable.java:12)
Exception in thread "main" java.lang.RuntimeException: finally中抛出异常
	at cn.huazai.main.throwable_example.TestThrowable.main(TestThrowable.java:17)

或者如果try catch finally中均有return,那么最终也只会返回finally中的内容

public class TestThrowable {
    public static void main(String[] args) {
        System.out.println(test1()); //2
        System.out.println(test2()); //2
        System.out.println(test3()); //2
    }

    static int test1() {
        try {
            return 1;
        }finally {
            return 2;
        }
    }
    static int test2() {
        try {
            throw new Exception();
        }finally {
            return 2;
        }
    }
    static int test3() {
        try {
            throw new Exception();
        }catch (Exception e) {
            return 1;
        }finally {
            return 2;
        }
    }
}

异常的转译和异常链

异常转译

更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法成为异常转译exception translation。

public class ThrowableTest02 {
    public static void main(String[] args) {
        try {
            Object obj = null;
            obj.toString();
        } catch (NullPointerException npe) { //底层的异常
            throw new RuntimeException("解释异常", npe); //转译成更高层的异常
        }
    }
}

运行结果:

Exception in thread "main" java.lang.RuntimeException: 解释异常
	at cn.huazai.main.throwable_example.ThrowableTest02.main(ThrowableTest02.java:15)
Caused by: java.lang.NullPointerException
	at cn.huazai.main.throwable_example.ThrowableTest02.main(ThrowableTest02.java:13)

异常转译不能滥用,如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。如果无法避免低层异常,次选方案是让高层悄悄绕过这些异常,从而将高层方法的调用者与低层的问题隔离开,在这种情况下,可以用适当的记录机制(如:java.util.logging)将异常记录下来。

异常链

一种特殊的异常转译的形式称为异常链,低层的异常原因被传到高层的异常,高层的异常提供访问方法来获得低层的异常。大多数标准的异常都有支持链的构造器,对于没有支持异常链的异常,可以利用Throwable的initCause方法设置原因。异常链不仅让你可以通过程序访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。

public class MyExceptionTest {
    public static void main(String[] args) {
        try {
            new TestMain().a();
        } catch (MyException2 e) {
            e.printStackTrace();
        }
    }
}
class MyException1 extends Exception {
}
class MyException2 extends Exception {
    public MyException2() {
        super();
    }
    public MyException2(Throwable cause) {
        super(cause);
    }
}
class TestMain {
    public void a() throws MyException2 {
        try {
            b();
        } catch (MyException1 e) {
            e.printStackTrace();
            throw new MyException2();
        }
    }

    public void b() throws MyException1 {
        throw new MyException1();
    }
}

运行结果:

cn.huazai.main.throwable_example.MyException1
	at cn.huazai.main.throwable_example.TestMain.b(MyExceptionTest.java:39)
	at cn.huazai.main.throwable_example.TestMain.a(MyExceptionTest.java:31)
	at cn.huazai.main.throwable_example.MyExceptionTest.main(MyExceptionTest.java:12)
cn.huazai.main.throwable_example.MyException2
	at cn.huazai.main.throwable_example.TestMain.a(MyExceptionTest.java:34)
	at cn.huazai.main.throwable_example.MyExceptionTest.main(MyExceptionTest.java:12)

总结:如果不能阻止或者处理来自更低层的异常,一般的做法是异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从底层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析

参考资料:

Java异常体系结构

Java异常体系结构详解

Java异常体系中的秘密

异常Exception