目录
try-with-resources语句
一些细节
新特性:try-with-resources中的实际变量
异常匹配
其他可选方式
检查型异常的一些观点
链式异常的使用
异常的使用指南
小结
本笔记参考自: 《On Java 中文版》
try-with-resources语句
层层叠叠的异常很容易让人看花眼,要确保每一条可能存在的故障路径无疑是一项巨大的挑战。在之前提到的InputFile.java就是一个反面教材。
可以改进InputFile:①将文件相关的操作(打开、读取和关闭)集中在构造器中,或是②使用Stream进行操作。
【例子:优化InputFile】
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.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("InputFile2.java").getLines().skip(14).limit(1).forEach(System.out::println);}
}
程序执行的结果是:
在上述代码中,getLines()只需要负责打开文件并创建流。
但实际上,这种问题并没有这么好回避。总是会有对象出乎我们的意料,这些对象往往需要在特定的时刻进行清理(比如走出某个作用域的时候)。
【例子:麻烦的异常处理】
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class MessyExceptions {public static void main(String[] args) {InputStream in = null;try {in = new FileInputStream(new File("MessyExceptions.java"));int contents = in.read();// 对内容进行操作} catch (IOException e) {// 处理错误} finally {if (in != null) {try {in.close();} catch (IOException e) {// close()可能报错,需要处理它}}}}
}
当我们终于进入了finally块,却发现自己可能还需要继续处理更多的异常。这就会让事情变得过于复杂。
为了简化这种重复的工作,Java 7引入了try-with-resources语法:
【例子:try-with-resources的使用例】
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class TryWithResources {public static void main(String[] args) {try (InputStream in = new FileInputStream(new File("TryWithResources.java"));) {int contents = in.read();// 进行各种操作} catch (IOException e) {// 处理错误}}
}
上述的try语法和之前有所不同,出现了一个():
括号中的内容叫做资源说明头,其中in的作用域是整个try块(包括下面的catch子句)。
更重要的是,在try-with-resources定义子句(即括号内)创建的对象必须实现java.lang.AutoCloseable接口,这个接口只有一个方法 —— close()。现在无论如何退出try块,都会执行操作in.close(),这就缩减了原本复杂的代码。
【例子:try-with-resources的使用例2】
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;public class StreamsAreAutoCloseable {public static void main(String[] args)throws IOException {try ( // 资源头可以包含多个定义,不同定义之间用分号隔开Stream<String> in = Files.lines(Paths.get("StreamsAreAutoCloseable.java"));PrintWriter outfile = new PrintWriter("Results.txt");) {in.skip(4).limit(1).map(String::toLowerCase).forEach(System.out::println);}}
}
在资源头中定义的每个对象,它们都会在try块的末尾调用对应的close()。
上述程序中,try块并没有对应的catch子句进行异常处理。这是因为IOException会直接通过main()传递出去,这就使得这一异常不需要在try块的末尾进行捕捉了。
顺便一提,Java 5实现的Closeable类后来也继承了AutoCloseable类。因此任何支持Closeable的对象也可以配合try-with-resources进行使用。
一些细节
可以创建自己的AutoCloseable类,来研究try-with-resources的底层机制:
class Reporter implements AutoCloseable {String name = getClass().getSimpleName();Reporter() {System.out.println("创建: " + name);}@Overridepublic void close() {System.out.println("关闭: " + 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();) {}}
}
程序执行的结果是:
在退出try块时,调用了两个对象的close()方法。由输出可以发现,会以和创建顺序相反的顺序关闭它们。这么做是考虑到不同的对象之间可能存在着依赖关系。
若某个类没有实现AutoCloseable接口,就会引发报错:
另外,让我们再来看看构造器报错的情况:
【例子:构造器报错(在资源说明头中抛出异常)】
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("在try块的内部");} catch (CE e) {System.out.println("捕获异常:" + e);}}
}
程序执行的结果是:
因为语句SecondExcept s = new SecondExcept();会抛出异常,所以编译器会强制要求我们提供一个catch子句来捕获它。这侧面反映了资源说明头实际上是被try块包围的。
仔细观察可以发现,SecondExcept的close()方法并没有被调用。这是因为构造已经失败了,我们无法假定在其上进行的任何操作是安全的。
------
【例子:在try块中抛出异常】
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("在try块中");Third t = new Third();new SecondExcept(); // 会抛出异常System.out.println("try块结束");} catch (CE e) {System.out.println("捕获异常:" + e);}}
}
程序执行的结果是:
注意:上述程序中的Third对象永远不会得到清理,因为它不是在资源说明头中进行创建的。
实际上,若是依赖于某个集成开发环境将代码重写为try-with-resources的形式,它们有可能只会保护所遇到的第一个对象,而忽略其他的对象。
------
【例子:close()抛出异常】
class CloseException extends Exception {
}class Reporter2 implements AutoCloseable {String name = getClass().getSimpleName();Reporter2() {System.out.println("创建:" + name);}@Overridepublic void close() throws CloseException {System.out.println("关闭:" + name);}
}class Closer extends Reporter2 {@Overridepublic 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("在try块中");} catch (CloseException e) {System.out.println("捕获异常:" + e);}}
}
程序执行的结果是:
一个好的习惯是将错误处理代码放置在catch子句中。
在这里,三个对象按照顺序创建,并且按照相反的顺序关闭,即使Closer.close()会抛出异常。
新特性:try-with-resources中的实际变量
最初,try-with-resources中的所有被管理的变量都需要被定义在资源说明头中。但从JDK 9开始,这些被管理的变量也可以被定义在try之前,只要它们是最终变量(或是实际上的最终变量)。
【例子:对比新旧语法】
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class EffectivelyFinalTWR {static void old() {try (InputStream r1 = new FileInputStream(new File("InputFile2.java"));InputStream r2 = new FileInputStream(new File("EffectivelyFinalTWR.java"))) {r1.read();r2.read();} catch (IOException e) {// 处理异常}}static void jdk9() throws IOException {// 最终变量final InputStream r1 = new FileInputStream(new File("InputFile2.java"));// 实际上的最终变量final InputStream r2 = new FileInputStream(new File("EffectivelyFinalTWR.java"));try (r1; r2) { // 将变量放入资源说明头r1.read();r2.read();} catch (IOException e) {System.out.println("在jdk9内部捕获异常:" + e);}// 此时r1和r2都被关闭// 但r1和r2还存在于作用域中,访问其中的任何一个都会引发异常r1.read();r2.read();}public static void main(String[] args) {old();try {jdk9();} catch (IOException e) {System.out.println("捕获异常:" + e);}}
}
程序执行的结果是:
jdk9()会把异常传递出来。这个特性无法捕获异常,所以它看起来不怎么可信。
异常匹配
当一个异常被抛出的时候,异常处理系统会按照处理程序的编写顺序寻找能够匹配异常的那个。当找到第一个匹配的处理程序时,系统会认为异常得到了处理,就不会进一步进行搜索了。
匹配异常不会要求完全匹配,子类的对象可以匹配其基类的处理程序:
【例子:异常匹配】
class Annoyance extends Exception {
}class Sneeze extends Annoyance {
}public class Human {public static void main(String[] args) {// 捕获精确的类型:try {throw new Sneeze();} catch (Sneeze s) {System.out.println("捕获异常,来自Sneeze");} catch (Annoyance a) {System.out.println("捕获异常,来自Annoyance");}// 捕获基类类型:try {throw new Sneeze();} catch (Annoyance a) {System.out.println("捕获异常,来自Annoyance");}}
}
程序执行的结果是:
上述代码中,catch (Annoyance a)将捕获Annoyance或者任何派生自它的类。
另外,若将基类异常的catch子句放在前面,子类的异常就永远无法被触发:
其他可选方式
异常处理允许我们的程序放弃正常语句序列的执行。但若需要处理每个调用可能产生的错误,就会显得过于繁琐。程序员不会这么做,但这会导致错误被忽略。注意,便于程序员处理错误是异常处理的主要动机之一。
异常处理有一个重要准则:除非知道怎么处理,否则不要捕获异常。
另外,通过允许一个处理程序应付多个出错点,异常往往也能减少错误处理的代码量。但检查型异常会使得这种情况变得更加复杂,因为它可能会强迫我们在无法处理错误的地方添加catch子句,这就会造成“吞食有害”:
try {// ...(一些有用的操作)
} catch(ObligatoryException e) {} // 强制性的异常处理
程序员往往只做最简单的事情,这就会造成在无意间“吞食”了异常。编译通过,此时除非进行复查并修正代码,否则异常就相当于丢失了。
一种处理方式是在处理程序中打印栈轨迹信息。尽管这种做法可以追踪异常,但这也表明我们没有真正理解如何在这个位置处理这个异常。
||| 格言:所有的模型都是错误的,但有些是有用的。
在评价Java的检查型异常时,应该牢记:这种异常应该能够引导程序员以更好的方式处理错误,并且不会增加太多的代码量。
检查型异常的一些观点
检查型异常是Java的一种尝试,实际上之后的编程语言也没有采用这种做法。
检查型异常或许能够在小型的程序中展示出其的妙用。但随着程序规模的增大,这种情况就会有所变化。有些语言可能不会适合大型项目,但却适合与小型项目。在增大的项目中,增多的检查型异常是难以控制的。
甚至有一种结论:在大型软件项目中,要求异常说明带来的结果是开发效率的降低,和代码质量几乎没有提高。
链式异常的使用
检查型异常的麻烦是需要解决的。这里有一种简单的解决方案,将一个检查型异常传递给RuntimeException构造器,就可以将这个异常包裹在RuntimeException中:
try {// ... 有用的处理
} catch(IDontKnowWhatToDoWithThisCheckedException e) {throw new RuntimeException(e); // 将e包裹进去,变成非检查型异常
}
由于异常链的存在,我们不会丢失任何来自原始异常的信息。
这使得我们有了选择:忽略这个异常,使其传递到更上层的上下文中;或者使用getCause()捕获和处理特定的异常。
【例子:异常链的使用】
import java.io.FileNotFoundException;
import java.io.IOException;class WrapCheckedException {void throwRuntimeException(int type) {try {switch (type) {case 0:throw new FileNotFoundException();case 1:throw new IOException();case 2:throw new RuntimeException("不断移动的RuntimeException异常");default:return;}} catch (IOException | RuntimeException e) {throw new RuntimeException(e); // 将检查型异常处理成非检查型异常}}
}class SomeOtherException extends Exception {
}public class TurnOffChecking {public static void main(String[] args) {WrapCheckedException wce = new WrapCheckedException();// 这里可以不使用try块// 通过直接调用throwRuntumeException(),可以让RuntimeException离开这个方法wce.throwRuntimeException(3);// 也可以选择捕获该异常:for (int i = 0; i < 4; i++) {try {if (i < 3)wce.throwRuntimeException(i);elsethrow new SomeOtherException();} catch (SomeOtherException e) {System.out.println("捕获异常SomeOtherException: " + e);} catch (RuntimeException re) {try {throw re.getCause();} catch (FileNotFoundException e) {System.out.println("捕获异常FileNotFoundException: " + e);} catch (IOException e) {System.out.println("捕获异常IOException: " + e);} catch (Throwable e) {System.out.println("捕获异常Throwable: " + e);}}}}
}
程序执行的结果是:
通过将异常捕获并包入RuntimeException,可以将检查型异常转换成运行时异常的cause。
另外,当我们准备捕捉异常的时候,我们通过会将代码放入一个try块中。此时我们仍然可以捕获任何我们想要的异常,首先捕捉我们明确知道的那些异常。当我们捕获RuntimeException时,通过抛出getCause()的结果,就可以提取出原始的异常了。
异常的使用指南
下面是一些异常使用的指南原则:
- 尽可能使用try-with-resources。
- 在恰当的层次处理问题(除非知道如何处理,否则不要捕获异常)。
- 可以使用异常修复问题,并重新调用引发异常的方法。
- 可以选择做好补救措施后继续,不再重写尝试引发异常的方法。
- 可以借助异常处理的过程计算戳某个结果,以替代方法本该生成的值。
- 可以在当前上下文完成能够完成的事,再将相同/不同的异常重新抛出。
- 使用异常来终止程序。
- 使用异常来简化问题(注意,若异常模式使问题变复杂了,用起来会非常麻烦)。
- 使用异常,使我们的库和程序更加安全(方便调试,提高程序稳健性)。
小结
异常使得我们可以集中精力在一个地方处理程序原本需要解决的问题,并在另一个地方处理来自代码的错误。异常的一个重要功能在于其的“报告”,Java坚持所有错误以异常的形式报告,这是一个优点。