Java高级程序设计

异常处理

异常处理

异常处理(Exception handling)是指在进行运算时,出现例外的情况(需要特殊处理的非常规或例外的情况)对应的处理,这种情况经常会破坏程序正常的流程。

它通常由特殊的编程语言结构、计算机硬件机制(如:中断或者如信号等操作系统IPC设施)所构成的。具体实现由硬件和软件自身定义而决定。一些异常,尤其是硬件,将会在被中断后进行恢复。

--wikipedia

看个例子

public class Calculator {

    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 2));
    }
    
}

看个例子

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}
❯ java Calculator
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Calculator.div(Calculator.java:4)
	at Calculator.main(Calculator.java:8)

ArithmeticException

public class ArithmeticException extends RuntimeException

Thrown when an exceptional arithmetic condition has occurred. For example, an integer "divide by zero" throws an instance of this class.

Since: JDK1.0

https://docs.oracle.com/javase/8/docs/api/java/lang/ArithmeticException.html

异常导致失去控制

public class Calculator {
    public int div(int a, int b) {
        int c = a / b;
        return c;
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}

return没有执行,div方法没执行完

C

int main()
{
   int i[2];
   i[3] = 10;
   return 0;
}
❯ gcc main.c
...
1 warning generated.
❯ ./a.out
[1]    20757 abort      ./a.out

异常处理

C以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。

如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。

-- 「On Java 8」

C++ 异常处理

#include <iostream>
using namespace std;

int main()
{
   int x = -1;
   try {
      if (x < 0) {
         throw x;
      }
   }
   catch (int x ) {
      cout << "Exception Caught \n";
   }
   return 0;
}

Java 异常处理

public class Calculator {
    public int div(int a, int b) {
        try{
            return a / b;
        }catch(Exception e){
            System.out.println("exception!");
            return 0; //这不合适,先将就一下
        }
    }

    public static void main(String[] args) {
        System.out.println(new Calculator().div(1, 0));
    }
}

运行

❯ java Calculator
exception!
0

程序运行完了!执行流程是符合预期的!

return 0是不合理的... 为什么?

从字节码层面看

javap -v -p -s -sysinfo -constants Calculator

  public int div(int, int);
    Code:
         0: iload_1
         1: iload_2
         2: idiv
         3: ireturn
         4: astore_3
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String exception!
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: iconst_0
        14: ireturn
      Exception table:
         from    to  target type
             0     3     4   Class java/lang/Exception
      LineNumberTable:
        line 4: 0

异常的语义

“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在那里将作出正确的决定。

在哪里做出正确的决定?

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        try{
            System.out.println(new Calculator().div(1, 0));
        }catch(Exception e){
            System.out.println("exception!");
        }
    }
}

再改一下

public class Calculator {
    public int div(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(Integer.parseInt(args[0]), Integer.parseInt(args[1])));
        } catch (Exception e) {
            System.out.println("exception!");
        }
    }
}

main()方法中try...catch...意味着此处定义了异常处理语义。

再改

public class Calculator {
    public int div(int a, int b) throws Exception {
        if (b == 0) {
            throw new Exception("divided by zero");
        }
        return a / b;
    }
    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(Integer.parseInt(args[0]), 
                                                    Integer.parseInt(args[1])));
        } catch (Exception e) {
            System.out.println("exception!");
        }
    }
}

从字节码层面看

  public int div(int, int) throws java.lang.Exception;
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=3
         0: iload_2
         1: ifne          14
         4: new           #2                  // class java/lang/Exception
         7: dup
         8: ldc           #3                  // String divided by zero
        10: invokespecial #4                  // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
        13: athrow
        14: iload_1
        15: iload_2
        16: idiv
        17: ireturn
      LineNumberTable:
        ...
    Exceptions:
      throws java.lang.Exception

类层次









https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html

Throwable

public class Throwable extends Object implements Serializable

The Throwable class is the superclass of all errors and exceptions in the Java language.

https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html

Error

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

https://docs.oracle.com/javase/8/docs/api/java/lang/Error.html

public static void print(String myString) {
    print(myString);
}
Exception in thread "main" java.lang.StackOverflowError
at StackOverflowErrorExample.print(StackOverflowErrorExample.java:6)

Exception

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.

The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions. Checked exceptions need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.

RuntimeException

if(t == null)
    throw new NullPointerException();

如果必须对传递给方法的每个引用都检查其是否为null(因为无法确定调用者是否传入了非法引用),这听起来着实吓人。幸运的是,这不必由你亲自来做,它属于 Java 的标准运行时检测的一部分。如果对null引用进行调用,Java 会自动抛出NullPointerException异常,所以上述代码是多余的,尽管你也许想要执行其他的检查以确保NullPointerException不会出现。

https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html

Checked Exception

Checked exceptions represent errors outside the control of the program. For example, the constructor of FileInputStream throws FileNotFoundException if the input file does not exist.

private static void checkedExceptionWithThrows() throws FileNotFoundException {
    File file = new File("not_existing_file.txt");
    FileInputStream stream = new FileInputStream(file);
}

Use a try-catch block to handle a checked exception.

finally

有一些代码片段,可能会希望无论 try 块中的异常是否抛出,它们都能得到执行。可以在异常处理程序后面加上 finally 子句。

try {
    // The guarded region: Dangerous activities
} catch(A a1) {
    // Handler for situation A
} finally {
    // Activities that happen every time
}

Why finally?

对于没有垃圾回收和析构函数自动调用机制的语言来说,finally 非常重要。它能使程序员保证:无论 try 块里发生了什么,内存总能得到释放。但 Java 有垃圾回收机制,所以内存释放不再是问题。而且,Java 也没有析构函数可供调用。那么,Java 在什么情况下才能用到 finally 呢?

当要把除内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。

自定义异常

class DividedByZeroException extends Exception{
    DividedByZeroException(){
        super("divided by zero");
    }
}
public class Calculator {
    public int div(int a, int b) throws DividedByZeroException {
        if (b == 0) {
            throw new DividedByZeroException();
        }
        return a / b;
    }
    ...
}

多重捕获

public class SameHandler {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process() {}
    void f() {
        try {
            x();
        } catch(Except1 e) {
            process();
        } catch(Except2 e) {
            process();
        } catch(Except3 e) {
            process();
        } catch(Except4 e) {
            process();
        }
    }
}

组合捕获

public class MultiCatch {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process() {}
    void f() {
        try {
            x();
        } catch(Except1 | Except2 | Except3 | Except4 e) {
            process();
        }
    }
}

Since Java 7

异常匹配

class SuperException extends Exception { }
class SubException extends SuperException { }
class BadCatch {
  public void goodTry() {
    try { 
      throw new SubException();
    } catch (SuperException superRef) { ...
    } catch (SubException subRef) {
      ...// never be reached
    } // an INVALID catch ordering
  }
}

栈轨迹

public class Calculator {
    public static void main(String[] args) {
        try {
            System.out.println(new Calculator().div(Integer.parseInt(args[0]), 
                                                    Integer.parseInt(args[1])));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
DividedByZeroException: divided by zero
        at Calculator.div(Calculator.java:4)
        at Calculator.main(Calculator.java:10)

重新抛出

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

catch(Exception e) {
    System.out.println("An exception was thrown");
    throw e;
}

Try-With-Resources

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
                }
            }
        }
    }
}

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();
        } catch(IOException e) {
            // Handle the error
        }
    }
}

Since Java 7 with AutoCloseable





“...on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth.”

--Martin Fowler

(author of UML Distilled, Refactoring, and Analysis Patterns)