Java异常详解

异常和错误

首先,我们需要重塑对异常和错误的理解,在我们的程序中,不存在所谓的“异常”,只存在错误,错误会导致我们的程序在运行期无法继续运行,如除数为零、尝试打开不存在的文件、试图使用指向为空的对象引用……
如果是上述任意一个情形发生,那么我们的程序都将无法继续运行,当然这些错误都是可以通过编码避免,但如果某一个方法的功能就是实现除法的功能,这个方法无法控制不接受获取除数为零的意外,也无法处理当接收到除数为零时的情况,那么这个时候,你需要从当前环境中抛出一个异常,通知其他环境,这里发生了一个问题,我无法处理,我将它交给你处理(不然程序就中止了)。到这里,大家大概知道了Java中的异常和问题的关系了,异常是Java为了解决在出现问题时发出的一个通知,这个通知到达它该到达的地方,然后在这个地方得到解决,然后使程序继续向错误发生时的情况运行。

Java中的异常

结构图


Java异常类结构图

Java异常类结构图

Error

Error用来表示编译时和系统错误,一般我们不用关心,这里再重申一个概念,异常是发生错误时被抛出的一个通知,所以Error是在编译时和系统错误时被抛出的异常。

Exception

这是我们需要关心的异常,因为当这些异常抛出时,说明了我们程序出现了问题,我们需要处理这些问题。Exception又分为不检查异常检查异常

不检查异常

public void method(String s) {  
    if(s == null)
    throw new NullPointerException();
}

首先,我们在代码中不会写这样的代码,因为这样的情况在我们的中可能数不胜数,如果都需要我们去手动的抛出异常,那么我们的代码会很难维护,那么当出现使用了指向空的引用时,系统会自动帮你抛出,所以我们不用手动抛出所有RuntimeException类型(它的子类)的异常。
其次,当有方法会抛出RuntimeException类型的异常时(系统自动抛出),我们不需要去捕获它(try-catch),所以这类异常被称为不检查异常。有经验的大家会知道,往往我们的程序的问题就是出现在这些不被检查的异常,如NullPointerException(经常在一些群中发现新手贴出空指针异常,然后求助……)。所以正因为RuntimeException类型的异常不用我们去捕获,因此我们在Code的时候一定要注意防止RuntimeException的发生。

检查异常

这一类异常抛出时,我们必须去捕获它,如SQLException、XMLStreamException等异常。至于说如何处理,根据具体的业务逻辑来编写处理的代码,那么接下来我们来看看Java中是如何设计异常处理的。

捕获异常

监控区域

如果在方法内部抛出了异常或者在方法内部调用的其他方法抛出了异常,这方法将在抛出异常后终止,如果不希望方法就此终止,那么在方法内设置一个特殊的块来捕获异常,所以称为try块

try {
    // Code that might generate exceptions
}

异常处理程序

再次强调一下,不被检查的异常编译时不会强制让我们捕获,所以需要大家自己注意这些异常。抛出的异常需要在某处得到处理,这个“地点”就是异常处理程序,而且在Java中,针对捕获到的不同异常,有不同的处理程序:

try {
    // Code that might generate exceptions
} catch(Type1 t1){
    // Handle exceptions of type1 
} catch(Type2 t2){
    // Handle exceptions of type2 
} catch(Type3 t3){
    // Handle exceptions of type3 
}

当发生异常事时(监控区域抛出异常),Java异常处理机制将负责搜寻catch中与异常类型相匹配的第一个处理程序,进入这个catch块,也就是说只有匹配的catch字句才能执行,即便是下面还有匹配的类型(为什么下面还会存在匹配的类型,因为类继承原因,下面还存在父类的异常),也不会执行,具有唯一匹配性。Java的这种异常捕获、处理的模式,可以很好的将正常的代码和出现问题时处理的代码分开,而不是混在一起。

finally

有一些代码,无论try中是否抛出异常,它们都能得到执行,这就是finally字句的作用。

try {
    // Code that might generate exceptions
} catch(Type1 t1){
    // Handle exceptions of type1 
} catch(Type2 t2){
    // Handle exceptions of type2 
} catch(Type3 t3){
    // Handle exceptions of type3 
} finally {

}

无论放生了什么,finally字句始终都会执行,即便是你在try或catch中加入了continue、break或者return。

finally用来做什么

Java中主要通过finally把资源恢复到它们的初始状态,如:已打开的文件或网络链接等,总言之,就是与外界“世界”的某个开关。这里有一个原则,就是在产生了一个必须被清理的对象之后,立即进入一个try-finally语句块,为什么会是在之后而不是把这个对象初始化也放进这个try中呢,因为finally总会执行,就会导致可能去做了释放没有被初始化的对象,这样会出现不良的代码结构。

终止模式

Java对于异常的处理采取的是终止模式,一旦发生问题,程序将不能继续执行,与之对应的是恢复模式,就是当异常抛出时,程序能够继续执行,而不是终止。在Java中如果我们要使用恢复模式,就需要将try块放在while循环中,直到满意,但这明显是不靠谱的,也是我们不提倡的。所以当当前方法终止时,我们只能在异常处理块中使程序向不同的方向继续执行,而具体向什么方向,取决于具体的实现。

异常说明

异常说明属于方法说明的一部分,紧跟在参数列表之后:

void method() throws IOException{
    //…………
}

如果某个方法有了异常说明,我们在使用的时候就必须捕获它,即使可能该方法根本不可能真实的抛出过该异常。当然,必须捕获的情况还要除去RuntimeException类型的异常(它的所有子类),因为他是不检查异常,即便是我们加了RuntimeException类型的异常说明,也是可以不去捕获的,所以我们加RuntimeException类型的异常说明是多此一举的(特别是对于不能看到方法源码的情况下)。

继承中的方法说明限制

在继承了某个类时,如果父类中某个方法(public或者protected)有异常说明,子类在覆盖方法时,异常说明只能是父类的一样的异常说明或者无异常说明,不能添加父类方法没有的异常说明。还有一个特殊情况,一个类的父类方法和接口方法同名,而且都添加了异常说明:

class A {
    void a() throws XPathException {
    }
}

interface B {
    void a() throws DataFormatException;
}

class AB extends A implements B {
    @Override
    public void a() {}

    // public void a() throws XPathException {} 不能编译
}

这种情况该方法不能添加任何异常说明。

异常类型

Java中已存在许多定义好的异常类型供我们使用,可通过查看Java文档查看,我们可以自己定义异常类,就必须从现有的异常类继承。那我们先来看看Throwable类相关的方法。

方法汇总

直到Java1.7,Throwable提供了4个公开的(public)和1个自身和子类可以访问(protected)的构造方法:

// 构造方法
// public 
Throwable() 
Throwable(String message)
Throwable(Throwable cause)
Throwable(String message, Throwable cause)

// protected 
Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)

addSuppressed(Throwable exception)
fillInStackTrace()
getCause()
getLocalizedMessage()
getMessage()
getStackTrace()
getSuppressed()
initCause(Throwable cause)
printStackTrace()
printStackTrace(PrintStream s)
printStackTrace(PrintWriter s)
setStackTrace(StackTraceElement[] stackTrace)
toString()

接下来通过一些例子来看看部分方法的使用:

方法使用例子

public class Test {
    public static void main(String[] args) {
        try {
            throw new Exception("My Exception");
        } catch (Exception ex) {
            System.out.println("getMessage(): " + ex.getMessage());
            System.out.println("getLocalizedMessage(): " + ex.getLocalizedMessage());
            System.out.println("toString(): " + ex.toString());
            System.out.println("printStackTrace(): ");
            ex.printStackTrace(System.out);
        }
    }
}/* Output:
getMessage(): My Exception
getLocalizedMessage(): My Exception
toString(): java.lang.Exception: My Exception
printStackTrace(): 
java.lang.Exception: My Exception
at com.exercise.Test.main(Test.java:13)
*///:~

在不改写getLocalizedMessage()方法时,getLocalizedMessage()和getMessage()获取的都是使用了带有参数的构造方法传入的信息,如上列为“My Exception”。toString()方法包含了getMessage()方法的信息,printStackTrace()方法则包含了toString()方法的内容,printStackTrace()方法打印的的是此异常调用栈的轨迹,显示了把你带到异常抛出点的方法调用序列,可以根据类名和行号具体定位。接下里我们结详细介绍一下printStackTrace()打印的栈轨迹。

栈轨迹

printStackTrace()提供的信息可以通过getStackTrace()方法获取到更详细的内容,该方法将返回一个数组,由栈轨迹元素(StackTraceElement)构成,每个元素表示栈中的一桢,元素0是栈顶元素,是方法调用序列中最后一个方法调用,也就是在这个方法中创建或抛出了异常。数组中最后一个元素(栈底)是调用序列中的第一个方法调用。如下:

public class Test {
    static void methodOne() {
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                System.out.println(stackTraceElement.getMethodName());
            }
        }
    }

    static void methodTwo() {
        methodOne();
    }

    static void methodThree() {
        methodTwo();
    }

    public static void main(String[] args) {
        methodOne();
        System.out.println("------------分割线------------");
        methodTwo();
        System.out.println("------------分割线------------");
        methodThree();
    }
}/* Output:
methodOne
main
------------分割线------------
methodOne
methodTwo
main
------------分割线------------
methodOne
methodTwo
methodThree
main
*///:~

此例只打印了方法名,还可以通过StackTraceElement答应更多的信息。其他方法不会专门为了介绍它而介绍,注意下面的内容,会不断地提到其他的方法。

重新抛出异常和异常链

有时我们在捕获到异常后,可能在捕获的地方不适合处理该异常,我们需要将它重新抛出:

catch(Exception e){
    throw e; 
} 

这样有一个好处,我们可以将异常交给上一级环境处理,但是这样就会存在一个问题,抛出的的异常携带的信息,也就是printStackTrace()方法显示的是原来异常抛出点的调用栈信息,而非重新抛出点的信息,这样重新抛出点的调用信息就被掩盖了。如果想更新重新抛出点信息到这个异常调用栈中,就可以使用fillInStackTrace()方法:

catch(Exception e){
    throw e.fillInStackTrace(); 
}

那么当前调用栈的信息就更新到了这个异常对象中了,还有一种情况,也会存在类似的丢失现象:

catch(Exception e){
    throw new Exception();
} 

这样我们上一级的抛出的异常信息就丢了,接收异常的地方就是只能得到new Exception()这个异常的信息。在JDK1.4以前如果你希望保存丢失的那个异常信息,只能通过编码的方式自己实现,而在JDK1.4后,Throwable类有一个构造方法接收一个Throwable类型的参数(文章上方方法汇总可以查看该构造方法)。那么这个传入的参数称为cause,它用来表示原始异常,那么就可以通过异常链从新的异常追踪到异常最初发生的位置。除了构造方法,我们还可以通过initCause(Throwable cause)方法传入一个Throwable对象,它的作用和构造函数传入一个Throwable对象是一样的。大家还记得之前介绍过finally字句吗?,它其实也会造成异常丢失:

class VeryImportantException extends Exception {
    @Override
    public String toString() {
        return "A very important exception!";
    }
}

class OtherException extends Exception {
    @Override
    public String toString() {
        return "Other exception";
    }
}

public class Test {
    void f() throws VeryImportantException {
        throw new VeryImportantException();
    }

    void dispose() throws OtherException {
        throw new OtherException();
    }

    public static void main(String[] args) {
        try {
            Test test = new Test();
            try {
                test.f();
            } finally {
                test.dispose();
            }
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}/* Output:
Other exception
*///:~

我们把最外一层try看着是上一级程序的处理,在这个try里面发生了两次异常,但是我们只能获得从finally中抛出的异常信息,而在f()方法中的异常信息丢失,这种情况我们称上一个异常被抑制了。这在JDK1.7之前同样需要我们自己编码去解决这个问题,在JDK1.7之后,新加入了两个方法帮助我们能够很好的去解决这个问题了,那就是addSuppressed(Throwable exception)和getSuppressed(),对于上述问题的解决:

class VeryImportantException extends Exception {
    @Override
    public String toString() {
        return "A very important exception!";
    }
}

class OtherException extends Exception {
    @Override
    public String toString() {
        return "Other exception";
    }
}

public class Test {
    void f() throws VeryImportantException {
        throw new VeryImportantException();
    }

    void dispose() throws OtherException {
        throw new OtherException();
    }

    public static void main(String[] args) {
        try {
            Test test = new Test();
            Exception exception = null;
            try {
                test.f();
            } catch (VeryImportantException e) {
                exception = e;
            } finally {
                try {
                    test.dispose();
                } catch (OtherException e) {
                    if (exception != null) {
                        exception.addSuppressed(e);
                    } else {
                        exception = e;
                    }
                }
                if (exception != null) {
                    throw exception;
                }
            }
        } catch (Exception e) {
            System.out.println(e);
            for (Throwable throwable : e.getSuppressed()) {
                System.out.println(throwable);
            }
        }
    }
}/* Output:
A very important exception!
Other exception
*///:~

Throwable JDK1.7新特性补充

同时捕获多个异常

try {
    Integer.parseInt("Hello");
} catch (NumberFormatException | RuntimeException e) {

}

这种情况不能将RuntimeException放在首位,编译不能通过。

try-with-resources

之前我们释放资源的需通过finally,但在JDK1.7后,可以使用try-with-resources方式,它可以实现自动释放功能,而不需要加上finally子句。需要做的就是将需要释放的资源对象放在try语句:

public String read(String filename) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
        StringBuilder builder = new StringBuilder();
        String line = null;
        while((line=reader.readLine())!=null){
            builder.append(line);
            builder.append(String.format("%n"));
        }
        return builder.toString();
    }
}

并且可以放入多个需要释放的资源:

public void copyFile(String fromPath, String toPath) throws IOException {
    try (InputStream input = new FileInputStream(fromPath);
         OutputStream output = new FileOutputStream(toPath)) {
        byte[] buffer = new byte[8192];
        int len = -1;
        while ((len = input.read(buffer)) != -1) {
            output.write(buffer, 0, len);
        }
    }
}

关于Java的异常就介绍到这里,希望大家能支持,你的支持是我继续前进的不断动力!

坚持原创分享,您的支持将鼓励我不断前行!