十五、异常(6)

本章概要

  • Try-With-Resources 用法
    • 揭示细节
  • 异常匹配

Try-With-Resources 用法

在考虑所有可能失败的方法时,找出放置所有 try-catch-finally 块的位置变得令人生畏。确保没有任何故障路径,使系统远离不稳定状态,这非常具有挑战性。

InputFile.java 是一个特别棘手的情况,因为文件被打开(伴随所有可能因此产生的异常),然后它在对象的生命周期中保持打开状态。每次调用 getLine()都可能导致异常,而且 dispose()也是这种情况。这个例子只是好在它显示了事情可以混乱到什么地步。它还表明了你应该尽量不要那样设计代码(当然,你经常会遇到这种无法选择的代码设计的情况,因此你仍然必须要理解它)。

InputFile.java 一个更好的实现方式是如果构造函数读取文件并在内部缓冲它 —— 这样,文件的打开,读取和关闭都发生在构造函数中。或者,如果读取和存储文件不切实际,你可以改为生成 Stream。理想情况下,你可以设计成如下的样子:

package base;// exceptions/InputFile2.java

import java.io.*;
import java.nio.file.*;
import java.util.stream.*;

public class InputFile2 {
    private String fname;

    public InputFile2(String fname) {
        this.fname = fname;
    }

    public Stream<String> getLines() throws IOException {
        return Files.lines(Paths.get(fname));
    }

    public static void main(String[] args) throws IOException {
        new InputFile2("D:\\onJava\\myTest\\base\\InputFile2.java").getLines()
                .skip(17)
                .limit(1)
                .forEach(System.out::println);
    }
}

输出为:

在这里插入图片描述

现在,getLines() 全权负责打开文件并创建 Stream。

你不能总是轻易地回避这个问题。有时会有以下问题:

  1. 需要资源清理
  2. 需要在特定的时刻进行资源清理,比如你离开作用域的时候(在通常情况下意味着通过异常进行清理)。

一个常见的例子是 java.io.FileInputStream 。要正确使用它,你必须编写一些棘手的样板代码:

import java.io.*;

public class MessyExceptions {
    public static void main(String[] args) {
        InputStream in = null;
        try {
            in = new FileInputStream(new File("MessyExceptions.java"));
            int contents = in.read();
            // Process contents
        } catch (IOException e) {
            // Handle the error
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // Handle the close() error
                }
            }
        }
    }
}

当 finally 子句有自己的 try 块时,感觉事情变得过于复杂。

幸运的是,Java 7 引入了 try-with-resources 语法,它可以非常清楚地简化上面的代码:

import java.io.*;

public class TryWithResources {
    public static void main(String[] args) {
        try (
                InputStream in = new FileInputStream(new File("TryWithResources.java"))
        ) {
            int contents = in.read();
            // Process contents
        } catch (IOException e) {
            // Handle the error
        }
    }
}

在 Java 7 之前,try 后面总是跟着一个 {,但是现在可以跟一个带括号的定义 ——这里是我们创建的 FileInputStream 对象。括号内的部分称为资源规范头(resource specification header)。现在 in 在整个 try 块的其余部分都是可用的。更重要的是,无论你如何退出 try 块(正常或通过异常),和以前的 finally 子句等价的代码都会被执行,并且不用编写那些杂乱而棘手的代码。这是一项重要的改进。

它是如何工作的? try-with-resources 定义子句中创建的对象(在括号内)必须实现 java.lang.AutoCloseable 接口,这个接口只有一个方法:close()。当在 Java 7 中引入 AutoCloseable 时,许多接口和类被修改以实现它;查看 Javadocs 中的 AutoCloseable,可以找到所有实现该接口的类列表,其中包括 Stream 对象:

StreamsAreAutoCloseable.java

import java.io.*;
import java.nio.file.*;
import java.util.stream.*;

public class StreamsAreAutoCloseable {
    public static void main(String[] args) throws IOException {
        try (
                Stream<String> in = Files.lines(Paths.get("D:\\onJava\\myTest\\base\\StreamsAreAutoCloseable.java"));
                PrintWriter outfile = new PrintWriter("D:\\onJava\\myTest\\base\\Results.txt"); // [1]
        ) {
            in.skip(5)
                    .limit(1)
                    .map(String::toLowerCase)
                    .forEachOrdered(outfile::println);
        } // [2]
    }
}

Results.txt

import java.nio.file.*;
  • [1] 你在这里可以看到其他的特性:资源规范头中可以包含多个定义,并且通过分号进行分割(最后一个分号是可选的)。规范头中定义的每个对象都会在 try 语句块运行结束之后调用 close() 方法。
  • [2] try-with-resources 里面的 try 语句块可以不包含 catch 或者 finally 语句而独立存在。在这里,IOException 被 main() 方法抛出,所以这里并不需要在 try 后面跟着一个 catch 语句块。

Java 5 中的 Closeable 已经被修改,修改之后的接口继承了 AutoCloseable 接口。所以所有实现了 Closeable 接口的对象,都支持了 try-with-resources 特性。

揭示细节

为了研究 try-with-resources 的基本机制,我们将创建自己的 AutoCloseable 类:

// exceptions/AutoCloseableDetails.java
class Reporter implements AutoCloseable {
    String name = getClass().getSimpleName();

    Reporter() {
        System.out.println("Creating " + name);
    }

    @Override
    public void close() {
        System.out.println("Closing " + name);
    }
}

class First extends Reporter {
}

class Second extends Reporter {
}

public class AutoCloseableDetails {
    public static void main(String[] args) {
        try (
                First f = new First();
                Second s = new Second()
        ) {
        }
    }
}

输出为:

十五、异常(6)_第1张图片

退出 try 块会调用两个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。顺序很重要,因为在这种情况下,Second 对象可能依赖于 First 对象,因此如果 First 在第 Second 关闭时已经关闭。 Second 的 close() 方法可能会尝试访问 First 中不再可用的某些功能。

假设我们在资源规范头中定义了一个不是 AutoCloseable 的对象

// exceptions/TryAnything.java
// {WillNotCompile}
class Anything {
}

public class TryAnything {
    public static void main(String[] args) {
        try (
                Anything a = new Anything()
        ) {
        }
    }
}

正如我们所希望和期望的那样,Java 不会让我们这样做,并且出现编译时错误。

如果其中一个构造函数抛出异常怎么办?

// exceptions/ConstructorException.java
class CE extends Exception {
}

class SecondExcept extends Reporter {
    SecondExcept() throws CE {
        super();
        throw new CE();
    }
}

public class ConstructorException {
    public static void main(String[] args) {
        try (
                First f = new First();
                SecondExcept s = new SecondExcept();
                Second s2 = new Second()
        ) {
            System.out.println("In body");
        } catch (CE e) {
            System.out.println("Caught: " + e);
        }
    }
}

输出为:

十五、异常(6)_第2张图片

现在资源规范头中定义了 3 个对象,中间的对象抛出异常。因此,编译器强制我们使用 catch 子句来捕获构造函数异常。这意味着资源规范头实际上被 try 块包围。

正如预期的那样,First 创建时没有发生意外,SecondExcept 在创建期间抛出异常。请注意,不会为 SecondExcept 调用 close(),因为如果构造函数失败,则无法假设你可以安全地对该对象执行任何操作,包括关闭它。由于 SecondExcept 的异常,Second 对象实例 s2 不会被创建,因此也不会有清除事件发生。

如果没有构造函数抛出异常,但在 try 的主体中可能抛出异常,那么你将再次被强制要求提供一个catch 子句:

// exceptions/BodyException.java
class Third extends Reporter {
}

public class BodyException {
    public static void main(String[] args) {
        try (
                First f = new First();
                Second s2 = new Second()
        ) {
            System.out.println("In body");
            Third t = new Third();
            new SecondExcept();
            System.out.println("End of body");
        } catch (CE e) {
            System.out.println("Caught: " + e);
        }
    }
}

输出为:

十五、异常(6)_第3张图片

请注意,第 3 个对象永远不会被清除。那是因为它不是在资源规范头中创建的,所以它没有被保护。这很重要,因为 Java 在这里没有以警告或错误的形式提供指导,因此像这样的错误很容易漏掉。实际上,如果依赖某些集成开发环境来自动重写代码,以使用 try-with-resources 特性,那么它们(在撰写本文时)通常只会保护它们遇到的第一个对象,而忽略其余的对象。

最后,让我们看一下抛出异常的 close() 方法:

// exceptions/CloseExceptions.java
class CloseException extends Exception {
}

class Reporter2 implements AutoCloseable {
    String name = getClass().getSimpleName();

    Reporter2() {
        System.out.println("Creating " + name);
    }

    @Override
    public void close() throws CloseException {
        System.out.println("Closing " + name);
    }
}

class Closer extends Reporter2 {
    @Override
    public void close() throws CloseException {
        super.close();
        throw new CloseException();
    }
}

public class CloseExceptions {
    public static void main(String[] args) {
        try (
                First f = new First();
                Closer c = new Closer();
                Second s = new Second()
        ) {
            System.out.println("In body");
        } catch (CloseException e) {
            System.out.println("Caught: " + e);
        }
    }
}

输出为:

十五、异常(6)_第4张图片

从技术上讲,我们并没有被迫在这里提供一个 catch 子句;你可以通过 main() throws CloseException 的方式来报告异常。但 catch 子句是放置错误处理代码的典型位置。

请注意,因为所有三个对象都已创建,所以它们都以相反的顺序关闭 - 即使 Closer.close() 抛出异常也是如此。仔细想想,这就是你想要的结果。但如果你必须亲手编写所有的逻辑,或许会丢失一些东西并使得逻辑出错。想想那些程序员没有考虑 Clean up 的所有影响并且出错的代码。因此,如果可以,你应当始终使用 try-with-resources。这个特性有助于生成更简洁,更易于理解的代码。

异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。

查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序,就像这样:

// exceptions/Human.java
// Catching exception hierarchies
class Annoyance extends Exception {
}

class Sneeze extends Annoyance {
}

public class Human {
    public static void main(String[] args) {
        // Catch the exact type:
        try {
            throw new Sneeze();
        } catch (Sneeze s) {
            System.out.println("Caught Sneeze");
        } catch (Annoyance a) {
            System.out.println("Caught Annoyance");
        }
        // Catch the base type:
        try {
            throw new Sneeze();
        } catch (Annoyance a) {
            System.out.println("Caught Annoyance");
        }
    }
}

输出为:

十五、异常(6)_第5张图片

Sneeze 异常会被第一个匹配的 catch 子句捕获,也就是程序里的第一个。然而如果将这个 catch 子句删掉,只留下 Annoyance 的 catch 子句,该程序仍然能运行,因为这次捕获的是 Sneeze 的基类。换句话说,catch(Annoyance a)会捕获 Annoyance 以及所有从它派生的异常。这一点非常有用,因为如果决定在方法里加上更多派生异常的话,只要客户程序员捕获的是基类异常,那么它们的代码就无需更改。

如果把捕获基类的 catch 子句放在最前面,以此想把派生类的异常全给“屏蔽”掉,就像这样:

try {
    throw new Sneeze();
} catch(Annoyance a) {
    // ...
} catch(Sneeze s) {
    // ...
}

此时,编译器会发现 Sneeze 的 catch 子句永远得不到执行,因此它会向你报告错误。

你可能感兴趣的:(#,On,Java,基础卷,异常匹配)