[关闭]
@Darcy 2017-08-11T03:13:35.000000Z 字数 8271 阅读 2138

第八章 程序错误--异常

JavaForKids

如果你在代码中少写了一个闭大括号,那么会导致编译错误,这样的错误很容易修复。但是如果你的程序是在运行时突然发生了错误,也就是我们说的运行时错误(run-time errors)。例如一个Java类在读取游戏的分数文件,而这时正好有个家伙删了它,那程序将会发生什么事呢? 是会弹出一大串错误信息崩溃掉?还是继续运行,然后显示友好的错误提示: “亲,由于某些原因,我不能读取到scores.txt文件,请确保存在这个文件!”? 你应该让你的程序处理好这些突发情况。在很多编程语言中,错误的处理都只能依靠程序员的良好习惯。但是Java则是会强制你去处理这些错误,否则连编译都不给你通过。

Java的运行时错误,称之为异常(Exceptions)。对错误的处理称之为异常处理。你必须在发生异常的地方加入 try-catch 块。下面的示例,就好像你告诉JVM:尝试读取文件中的分数,如果发生错误,那就捕获这个错误并执行处理这个错误的代码 :

  1. try{
  2. fileScores.read();
  3. }
  4. catch (IOException e){
  5. System.out.println("Dear friend,I could not read the file cores.txt");
  6. }

在这里我们只需要知道什么是I/O ( Input/Output) 就可以了。从磁盘或者其他设备中读取或者写入数据相应称为输入(Input)和输出(Output),这个过程可能会引起IOExceptionIOException这个类包含了输入/输入相关的错误信息。

方法会在错误发生时抛出异常,不同类型的错误会对于不同类型的异常。如果catch块包含了某个指定的异常类型,则当程序发生这个错误的时候,程序就会跳入catch块去执行里面的代码,程序依然会继续运行下去,这个错误也会得到处理。

上面代码里的输出的语句只会在文件读取发生错误的时候才会被执行。

1. 读取堆栈轨迹

如果程序发生了一个未知的异常,而且没有捕获处理,那么它就会在屏幕上打印出很多行的错误信息,这些信息就是堆栈轨迹。如果你的程序在发生错误前调用了许多方法,那么堆栈轨迹可以帮助你追踪到发生错误的代码。

现在让我们来写个程序TestStackTrace,并让它故意去除0。

  1. public class TestStackTrace {
  2. TestStackTrace() {
  3. divideByZero();
  4. }
  5. int divideByZero() {
  6. return 25 / 0;
  7. }
  8. public static void main(String[] args) {
  9. new TestStackTrace();
  10. }
  11. }

这个程序展示了在出现错误时方法被调用的顺序。我们要从最后的一行读起。

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at TestStackTrace.divideByZero(TestStackTrace.java:7)
    at TestStackTrace.<init>(TestStackTrace.java:3)
    at TestStackTrace.main(TestStackTrace.java:11)

这个信息显示了这个程序是由 main() 方法开始的,然后是构造器 init(),然后调用 divideByZero()。行数 11 ,3 和 7 是程序调用相应方法在代码的所在行数。在这之后,当程序跑到第九行的时候,尝试去除以0,这时候抛出了一个 ArithmeticException

2. 异常的继承树

Java 的异常也是类。下图展示了部分异常及其之间的继承关系:

程序员是如何及时地知道一些Java 方法会抛出一个异常或者是应该使用try/catch 块?不用担心,如果你调用一个可能会抛出异常的方法,Java 编译器会打印一个类似于下面的错误信息:

"ScoreReader.java": unreported exception: java.io.IOException;
must be caught or declared to be thrown at line 57

当然,你也可以去查阅关于会抛出异常的方法的Java文档。这一章接下来的内容,将会解释如何去处理异常。

3. Try/Catch 块

用于处理错误的Java关键字有五个:
trycatch , finally , throw , throws

在一个try 块后,如果你觉得会发生不只一个错误,你可以放置多个catch 块。比如,当程序去读取一个文件的时候,这个文件可能不存在,然后会抛出 FileNotFoundException;或者是,这个文件是存在的,但是程序已经读到了这个文件的末尾,但仍继续去读取这个文件,这时候会产生EOFException。下面的代码在找不到文件或者是读到了文件末尾的时候,会打印相关的解释信息。对于其他因为读取而导致的错误,它都会打印该错误的具体描述。

  1. public void getScores(){
  2. try{
  3. fileScores.read();
  4. System.out.println(“Scores loaded successfully”);
  5. }catch(FileNotFoundException e){
  6. System.out.println(“Can not find file Scores”);
  7. }catch(EOFException e1){
  8. System.out.println(“Reached end of file”);
  9. }catch(IOException e2){
  10. System.out.println(“Problem reading file +
  11. e2.getMessage());
  12. }
  13. }

如果read() 方法执行失败,程序会跳过不执行System.out.println(“Scores loaded successfully) ,并且会尝试定位到对应错误的 catch 块。如果程序找到了符合的catch 块,那么就执行相应的println() 。如果没有找到,那么getScores() 方法会给调用它的方法再次抛出这个异常。

如果你写了多个catch块,你需要根据这些异常的相互继承关系去放置顺序。例如,EOFExceptionIOException的子类,你必须优先放置这些异常的子类。如果你先放置IOException,因为IOEception 拦截了FileNotFoundException或者是EOFException,此时程序永远也不会执行 FileNotFoundException或者是EOFExceptioncatch 块。

懒人可能会这样去编写 getScores():

  1. public void getScores(){
  2. try{
  3. fileScores.read();
  4. }catch(Exception e){
  5. System.out.println(“Problem reading file ”+
  6. e.getMessage());
  7. }
  8. }

这是个糟糕的代码风格的示例。当你在写程序的时候,始终要牢记一点,如果你不想为你自己写的代码感到羞愧的话,一定要确保除了你之外的人能读懂。

catch 块会收到含有错误信息的异常对象的实例,这个异常对象的getMessage()会返回错误信息。有时候,你觉得通过这个方法得到的信息不够全面,你可以尝试用toString() 替代:

  1. catch(Exception e){
  2. System.out.println(“Problem reading file ”+ e.toString());
  3. }

或者你需要关于这个异常的更多的详细的信息,请使用printStackTrace()。它会像读取堆栈轨迹小节里的示例一样,打印出方法调用的顺序。

我们来尝试下“杀死”第六章的计算器程序。运行Calculator,通过键盘输入字符 abc,然后按下任何响应按键,接下来你会在终端界面会看到类似下面的信息:

java.lang.NumberFormatException: For input string: "abc"
at
java.lang.NumberFormatException.forInputString(NumberFormatExeption.java:48)
at
java.lang.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1213)
at java.lang.Double.parseDouble(Double.java:202)
at
CalculatorEngine.actionPerformed(CalculatorEngine.java:27)
at
javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1764)

这时一个没有处理异常的例子。CalculatorEngine类里的actionPerformed()方法里有下面的代码:

  1. displayValue= Double.parseDouble(dispFieldText);

如果变量 diapFieldTest 不是一个数值,parseDouble()方法将不能把它转化成 double 数据类型,并且会抛出NumberFormatException

我们现在来写处理这个异常,并给用户展示关于这个错误的提示。Eclipse 会帮助你把parseDouble()这一行必须放置在try/catch 块中。定位到这一行,并且用鼠标右键点击这一行,在弹出的窗口中选择Source and Surround with try/catch block。就是这样,更改后的代码是这样的:

  1. try {
  2. displayValue= Double.parseDouble(dispFieldText);
  3. } catch (NumberFormatException e1) {
  4. // TODO Auto-generated catch block
  5. e1.printStackTrace();
  6. }

我们用下面的代码替换 printStackTrace()

  1. javax.swing.JOptionPane.showConfirmDialog(null,
  2. "Please enter a Number", "Wrong input",javax.swing.JOptionPane.PLAIN_MESSAGE);
  3. return;

我们将用一个浅显易懂的信息—Please enter a Number,来替换一堆令人抓狂的错误的堆栈轨迹:


现在,NumberFormatException 这个已经就已经被处理了。

4. 关键字 throws

在某些情况下,在异常方法的调用方法中处理异常,会比在发生异常的方法中处理更有意义。

这些方法需要在方法签名中声明它可能会抛出一些特定的异常,这个可以通过关键字throws来做到。我们依旧使用前面读文件的例子来说明。我们已经知道read()方法会抛出IOException异常, 那么你要么处理它,或者声明它。在下面的的例子中我们将会声明在getAllScores()可能会发生IOException异常:

  1. class MySuperGame {
  2. void getAllScores() throws IOException {
  3. // 如果你不打算在这里处理异常,那就不要在这里使用try/catch,而是直接抛出
  4. file.read();
  5. }
  6. public static void main(String[] args) {
  7. MySuperGame msg = new MySuperGame();
  8. System.out.println("List of Scores");
  9. try {
  10. // 既然getAllScores()声明了异常,我们就在这里处理它吧
  11. msg.getAllScores();
  12. } catch (IOException e) {
  13. System.out.println("Sorry, the list of scores is not available");
  14. }
  15. }
  16. }

即使我们不在读取的时候捕获异常,这时候IOException 也会从getAllScores()传递到调用它的main()方法中。可以看到,我们在main()方法中处理了这个异常。

5. 关键字 finally

try/catch块中的代码,会以以下的方式结束:

  1. try代码块中的代码正常运行,程序继续运行;
  2. try代码块中通过return返回,退出该方法。
  3. try代码块中的代码抛出一个异常,并且运行匹配到该异常的catch块中的代码,然后在catch里面可以处理错误,让该方法继续运行,也可以把该异常重新抛出给该方法的调用者。

如果不管发生什么,一些代码必须要执行的,请把这些代码放在关键字 finally之后:

  1. try{
  2. file.read();
  3. }catch(Exception e){
  4. printStackTrace();
  5. }finally{
  6. // 必须要得到执行的代码可以放在这里,比如说file.close();
  7. }

不管read方法执行失败还是成功,上面的代码必须去关闭文件。通常,我们在finally块中执行释放计算机系统的资源的代码。比如断开网络连接或者是关闭文件。

如果你不准备处理这些异常,那么它们会传递到调用这些方法的地方。这种情况下,你可以只使用finally,而不用加上catch块。

  1. void myMethod () throws IOException{
  2. try{
  3. // 读取文件的代码放在这里
  4. }finally{
  5. // 关闭文件的代码放在这里
  6. }
  7. }

6. 关键字 throw

如果一个方法发生了异常,而你觉得应该把这个异常交给它的调用者来处理,那么,你只需要把这个异常重新抛出就可以了。有时候,你可能想捕获其中的一个异常,然后重新抛出一个自定义描述的异常,正如下面中的代码片段所描述的。

throw语句是用来抛出异常对象的,这个对象必须是Trowable的子类。在Java中所有的异常都是Throwable的子类。

接下来的代码片段,getAllScores()方法捕获了IOException,同时新建了一个含有更多友好的错误描述的Exception对象,然后再次把这个异常抛给main() 方法。现在,因为getAllScores()方法会抛出一个异常,你应该去处理或者是再次抛出。如果你不把调用 getAllScores()这一行放在try/catch块中,main() 方法是编译不过的。你应该处理这个异常,因为main()方法不应该抛出异常的。

  1. class ScoreList{
  2. static void getAllScores() throws Exception{
  3. try{
  4. file.read();//这一行可能会产生一个异常
  5. } catch (IOException e) {
  6. throw new Exception ("Dear Friend, the file Scores has problems");
  7. }
  8. }
  9. public static void main(String[] args){
  10. System.out.println("Scores");
  11. try{
  12. getAllScores();
  13. }catch(Exception e1){
  14. System.out.println(e1.getMessage());
  15. }
  16. }
  17. }

main()方法会处理 文件错误的情况,el.getMessage()方法回返回如Dear Friend... 的信息。

7. 创建一个新的异常

程序员可以创建Java系统中没有的新的异常类。一般自定义的异常类会从已有的Java异常类中派生。假设: 你正在销售自行车,你必须验证客户订单。我们新建一个Exception的子类叫做 TooManyBikesException。如果一个客户尝试购买超过3辆自行车,那么就会抛出这个异常:

  1. class TooManyBikesException extends Exception{
  2. // 构造方法
  3. TooManyBikesException (){
  4. // 只调用父类的构造方法,并把错误显示信息传递给它
  5. super("Can't ship this many bikes in one shipment.");
  6. }
  7. }

TooManyBikesException这个类是只有一个构造器,并把错误的描述传递给它的父类。当catch块接收到这个异常,通过调用getMessage()就可以知晓发生了什么。

设想一个用户在OrderWindow中选择了几辆自行车,并且点击了Place Order按键。通过第六章的学习,我们知道这个动作会调用actionPerformed()这个方法。它会检查这个订单是否应该下发。checkOrder方法会检查订单是否符合条件,如果订单数量超过3则抛出TooManyBikesException异常。如果购物车里的订单不符合条件,actionPerformed方法就会在catch块中拦截这个异常,并且在窗口中的文字区域中显示错误信息。

  1. class OrderWindow implements ActionListener {
  2. String selectedModel;
  3. String selectedQuantity;
  4. int quantity;
  5. void actionPerformed(ActionEvent e) {
  6. try {
  7. selectedModel = txtFieldModel.getText();
  8. selectedQuantity = txtFieldQuantity.getText();
  9. quantity = Integer.parseInt(selectedQuantity);
  10. //这里可能会抛出TooManyBikesException的异常
  11. bikeOrder.checkOrder(selectedModel, quantity);
  12. txtFieldOrderConfirmation.setText("Your order is complete");
  13. } catch (TooManyBikesException e) {
  14. txtFieldOrderConfirmation.setText(e.getMessage());
  15. }
  16. }
  17. void checkOrder(String bikeModel, int quantity)throws TooManyBikesException {
  18. // 在这里检查订单上bikeModel类型的车数量quantity是否超过3辆,如果是就抛出TooManyBikesException异常
  19. if(quantity > 3){
  20. throw new TooManyBikesException("Can not ship" + quantity+ " bikes of the model " + bikeModel + " in one shipment");
  21. }
  22. }
  23. }

在理想的情况下,每一个程序都应该正常运行。但是我们必须做好面对一些不希望遇到的情况的准备。Java强制你必须为这些情况写相应的代码确实很有帮助。

8. 扩展阅读

  1. 通过异常处理错误
    http://java.sun.com/docs/books/tutorial/essential/exceptions/

9. 练习

  1. 为自行车订单创建一个图形应用。它必须有BikeModelQuantity两个输入文本框,一个Place Order按键和一个订单确认的标签。

  2. 使用示例中的OrderWindowTooManyBikesException。创建几种会导致抛出异常的自行车车型和数量的组合。

10. 进一步的练习

  1. 使用包含几种自行车车型的下拉框替代上面练习中的文本框,用户在下拉框中选择比输入会有更好的体验。你可以从网络上查找图形组件 JComboBox 和用来监听用户选择自行车车型的事件的ItemListener相关知识 。

創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 2.5 中國大陸 授權條款授權.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注