🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## [构造器](https://lingcoder.gitee.io/onjava8/#/book/15-Exceptions?id=%e6%9e%84%e9%80%a0%e5%99%a8) 有一点很重要,即你要时刻询问自己“如果异常发生了,所有东西能被正确的清理吗?"尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出了异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器时要格外细心。 你也许会认为使用 finally 就可以解决问题。但问题并非如此简单,因为 finally 会每次都执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在 finaly 子句中却是要被清理的。 在下面的例子中,建立了一个 InputFile 类,它能打开一个文件并且每次读取其中的一行。这里使用了 Java 标准输入/输出库中的 FileReader 和 BufferedReader 类(将在[附录:I/O 流](https://lingcoder.gitee.io/onjava8/#/./Appendix-IO-Streams)中讨论),这些类的基本用法很简单,你应该很容易明白: ~~~ // exceptions/InputFile.java // Paying attention to exceptions in constructors import java.io.*; public class InputFile { private BufferedReader in; public InputFile(String fname) throws Exception { try { in = new BufferedReader(new FileReader(fname)); // Other code that might throw exceptions } catch(FileNotFoundException e) { System.out.println("Could not open " + fname); // Wasn't open, so don't close it throw e; } catch(Exception e) { // All other exceptions must close it try { in.close(); } catch(IOException e2) { System.out.println("in.close() unsuccessful"); } throw e; // Rethrow } finally { // Don't close it here!!! } } public String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { throw new RuntimeException("readLine() failed"); } return s; } public void dispose() { try { in.close(); System.out.println("dispose() successful"); } catch(IOException e2) { throw new RuntimeException("in.close() failed"); } } } ~~~ InputFile 的构造器接受字符串作为参数,该字符串表示所要打开的文件名。在 try 块中,会使用此文件名建立 FileReader 对象。FileReader 对象本身用处并不大,但可以用它来建立 BufferedReader 对象。注意,使用 InputFile 的好处之一是把两步操作合而为一。 如果 FileReader 的构造器失败了,将抛出 FileNotFoundException 异常。对于这个异常,并不需要关闭文件,因为这个文件还没有被打开。而任何其他捕获异常的 catch 子句必须关闭文件,因为在它们捕获到异常之时,文件已经打开了(当然,如果还有其他方法能抛出 FileNotFoundException,这个方法就显得有些投机取巧了。这时,通常必须把这些方法分别放到各自的 try 块里),close() 方法也可能会抛出异常,所以尽管它已经在另一个 catch 子句块里了,还是要再用一层 try-catch,这对 Java 编译器而言只不过是多了一对花括号。在本地做完处理之后,异常被重新抛出,对于构造器而言这么做是很合适的,因为你总不希望去误导调用方,让他认为“这个对象已经创建完毕,可以使用了”。 在本例中,由于 finally 会在每次完成构造器之后都执行一遍,因此它实在不该是调用 close() 关闭文件的地方。我们希望文件在 InputFlle 对象的整个生命周期内都处于打开状态。 getLine() 方法会返回表示文件下一行内容的字符串。它调用了能抛出异常的 readLine(),但是这个异常已经在方法内得到处理,因此 getLine() 不会抛出任何异常。在设计异常时有一个问题:应该把异常全部放在这一层处理;还是先处理一部分,然后再向上层抛出相同的(或新的)异常;又或者是不做任何处理直接向上层抛出。如果用法恰当的话,直接向上层抛出的确能简化编程。在这里,getLine() 方法将异常转换为 RuntimeException,表示一个编程错误。 用户在不再需要 InputFile 对象时,就必须调用 dispose() 方法,这将释放 BufferedReader 和/或 FileReader 对象所占用的系统资源(比如文件句柄),在使用完 InputFile 对象之前是不会调用它的。可能你会考虑把上述功能放到 finalize() 里面,但我在[封装](https://lingcoder.gitee.io/onjava8/#/./Housekeeping)讲过,你不知道 finalize() 会不会被调用(即使能确定它将被调用,也不知道在什么时候调用),这也是 Java 的缺陷:除了内存的清理之外,所有的清理都不会自动发生。所以必须告诉客户端程序员,这是他们的责任。 对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的 try 子句: ~~~ // exceptions/Cleanup.java // Guaranteeing proper cleanup of a resource public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); try { String s; int i = 1; while((s = in.getLine()) != null) ; // Perform line-by-line processing here... } catch(Exception e) { System.out.println("Caught Exception in main"); e.printStackTrace(System.out); } finally { in.dispose(); } } catch(Exception e) { System.out.println( "InputFile construction failed"); } } } ~~~ 输出为: ~~~ dispose() successful ~~~ 请仔细观察这里的逻辑:对 InputFile 对象的构造在其自己的 try 语句块中有效,如果构造失败,将进入外部的 catch 子句,而 dispose() 方法不会被调用。但是,如果构造成功,我们肯定想确保对象能够被清理,因此在构造之后立即创建了一个新的 try 语句块。执行清理的 finally 与内部的 try 语句块相关联。在这种方式中,finally 子句在构造失败时是不会执行的,而在构造成功时将总是执行。 这种通用的清理惯用法在构造器不抛出任何异常时也应该运用,其基本规则是:在创建需要清理的对象之后,立即进入一个 try-finally 语句块: ~~~ // exceptions/CleanupIdiom.java // Disposable objects must be followed by a try-finally class NeedsCleanup { // Construction can't fail private static long counter = 1; private final long id = counter++; public void dispose() { System.out.println( "NeedsCleanup " + id + " disposed"); } } class ConstructionException extends Exception {} class NeedsCleanup2 extends NeedsCleanup { // Construction can fail: NeedsCleanup2() throws ConstructionException {} } public class CleanupIdiom { public static void main(String[] args) { // [1]: NeedsCleanup nc1 = new NeedsCleanup(); try { // ... } finally { nc1.dispose(); } // [2]: // If construction cannot fail, // you can group objects: NeedsCleanup nc2 = new NeedsCleanup(); NeedsCleanup nc3 = new NeedsCleanup(); try { // ... } finally { nc3.dispose(); // Reverse order of construction nc2.dispose(); } // [3]: // If construction can fail you must guard each one: try { NeedsCleanup2 nc4 = new NeedsCleanup2(); try { NeedsCleanup2 nc5 = new NeedsCleanup2(); try { // ... } finally { nc5.dispose(); } } catch(ConstructionException e) { // nc5 const. System.out.println(e); } finally { nc4.dispose(); } } catch(ConstructionException e) { // nc4 const. System.out.println(e); } } } ~~~ 输出为: ~~~ NeedsCleanup 1 disposed NeedsCleanup 3 disposed NeedsCleanup 2 disposed NeedsCleanup 5 disposed NeedsCleanup 4 disposed ~~~ * \[1\] 相当简单,遵循了在可去除对象之后紧跟 try-finally 的原则。如果对象构造不会失败,就不需要任何 catch。 * \[2\] 为了构造和清理,可以看到将具有不能失败的构造器的对象分组在一起。 * \[3\] 展示了如何处理那些具有可以失败的构造器,且需要清理的对象。为了正确处理这种情况,事情变得很棘手,因为对于每一个构造,都必须包含在其自己的 try-finally 语句块中,并且每一个对象构造必须都跟随一个 try-finally 语句块以确保清理。 本例中异常处理的混乱情形,有力的论证了应该创建不会抛出异常的构造器,尽管这并不总会实现。 注意,如果 dispose() 可以抛出异常,那么你可能需要额外的 try 语句块。基本上,你应该仔细考虑所有的可能性,并确保正确处理每一种情况。