[关闭]
@lemonguge 2017-09-15T02:40:15.000000Z 字数 8595 阅读 374

异常处理

JAVA


异常就是Java通过面向对象的思想将问题封装成了对象。

以前正常流程代码和问题处理代码相结合,使用异常能将正常流程代码和问题处理代码分离,降低了代码的复杂度,提高阅读性。

在Java中用异常类对问题进行描述,不同的问题用不同的类进行具体的描述。问题很多,意味着描述的类也很多,将其共性进行向上抽取,从而形成了异常体系。在之前介绍的继承中,我们应该知道学习体系中顶层的类可以了解该体系的基本功能,创建体系中的最子类对象,完成功能的使用。

Throwable(可抛出的)这个Java类被用来表示任何可以作为异常被抛出的类。

Throwable这个类又有两个子类:ErrorException。无论是错误,还是异常,当问题发生就应该可以抛出,让调用者知道并处理。

  1. public class Error {
  2. public static void main(String[] args) {
  3. byte b = 128; // 128超出了byte的范围
  4. }
  5. } /* Output:
  6. Exception in thread "main" java.lang.Error: Unresolved compilation problem:
  7. Type mismatch: cannot convert from int to byte
  8. at Error.main(Error.java:3)
  9. *///:~
  1. public static void main(String[] args){
  2. Exception e = new Exception(); //仅创建了一个异常对象,并进行没有处理
  3. System.out.println(e);
  4. } /* Output:
  5. java.lang.Exception
  6. *///:~

当异常发生时,可以将这个异常对象通过throw关键字进行抛出,异常对象将从当前方法这一层到上一层(调用此方法的那一层),当抛出了异常之后就会退出当前作用域。所以在throw语句之后不能代码,否则会因为不能被执行而发生编译期错误。

  1. public class Except {
  2. public static void main(String[] args) throws Exception { //声明异常
  3. Exception e = new Exception();
  4. throw e; //将异常对象抛出,异常对象将被JVM接收
  5. // ! System.out.println("不能被执行"); // Unreachable code
  6. }
  7. } /* Output:
  8. Exception in thread "main" java.lang.Exception
  9. at Except.main(Except.java:3)
  10. *///:~

自定义异常

Java提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常类来表示程序中可能会遇到的特定问题。

通常对异常来说,最重要的信息就是异常类型名。

错误信息可以保存在异常对象内部或者用异常类的名称来暗示,上一层环境通过这些信息来决定如何处理异常。

  1. class MyException extends Exception { } // 异常类型名是此自定义异常类型仅有的信息
  2. public class Except {
  3. public static void show() throws MyException { // 声明异常
  4. MyException e = new MyException();
  5. throw e;
  6. }
  7. public static void main(String[] args) throws MyException { // 声明异常
  8. show();
  9. }
  10. } /* Output:
  11. Exception in thread "main" MyException
  12. at Except.show(Except.java:5)
  13. at Except.main(Except.java:9)
  14. *///:~
  1. class MyException extends Exception {
  2. public MyException(String message) {
  3. super(message); // 明确调用了基类的构造器,接受一个字符串作为参数。
  4. }
  5. }
  6. public class Except {
  7. public static void show() throws MyException { // 声明异常
  8. MyException e = new MyException("自定义类型");
  9. throw e;
  10. }
  11. public static void main(String[] args) { // 捕获异常
  12. try {
  13. show();
  14. } catch (MyException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. } /* Output:
  19. MyException: 自定义类型
  20. at Except.show(Except.java:12)
  21. at Except.main(Except.java:18)
  22. *///:~

编译期异常的处理方式

对抛出的异常进行处理有两种方式:声明异常或捕获异常。

有一个很好的比喻来说明上面这句话,相信读者看完之后会有感触的。

有一个人带了两手雷准备坐飞机,你作为安检人员,发现这个人带了两手雷。throw new Exception("两个手雷");

声明异常

通过使用throws关键字在函数上对可能会抛出的异常类型进行声明。

  1. public class Except {
  2. public static void show() throws Exception { // 声明异常
  3. Exception e = new Exception("声明异常");
  4. throw e; //抛出了异常,要对该异常有对应的预先处理方式
  5. }
  6. public static void main(String[] args) throws Exception { // 声明异常
  7. show();
  8. }
  9. } /* Output:
  10. Exception in thread "main" java.lang.Exception: 声明异常
  11. at Except.show(Except.java:3)
  12. at Except.main(Except.java:7)
  13. *///:~

throw与throws关键字

  1. throws使用在函数上(extendsimplements用于类上),throw使用在函数内。
  2. throws抛出的是异常类,可以抛出多个,用逗号隔开,throw抛出的是异常对象。

捕获异常

通过使用catch关键字对抛出的异常进行捕获,从而可以进行下一步的处理。

  1. public class Except {
  2. public static void show() throws Exception { // 声明异常
  3. Exception e = new Exception("声明异常");
  4. throw e;
  5. }
  6. public static void main(String[] args) { // 声明异常
  7. try {
  8. show();
  9. } catch (Exception e) { //捕获了异常
  10. System.out.println("捕获异常");
  11. }
  12. }
  13. } /* Output:
  14. 捕获异常
  15. *///:~

try-catch-finally代码块

要明白异常是如何被捕获的,必须首先理解监控区域的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

try代码块

在这个代码块中“尝试”各种可能产生异常的方法调用。

  1. try {
  2. // Code that might generate exceptions
  3. }

catch代码块

catch代码块也称为异常处理程序,派生异常类的对象可以匹配其基类的处理程序

每一次“捕获”一种匹配的异常并进行相应的处理,必须在try之后,可以有多个catch代码块(异常处理程序列表)。

  1. try {
  2. // Code that might generate exceptions
  3. } catch (Type1 id1) {
  4. // Handle exceptions of Type1
  5. } catch (Type2 id2) {
  6. // Handle exceptions of Type2
  7. } catch (Type3 id3) {
  8. // Handle exceptions of Type3
  9. }

可以在处理程序的内部使用标识符(id1id2等等),这与方法的形式参数的使用很相似。异常处理机制将负责把捕获的异常对象的类型与这些catch中的类型进行顺序匹配,当第一次匹配成功后,便进入catch子句执行,此时认为异常得到了处理,匹配结束,不会进行下一次匹配。

  1. class MyException1 extends Exception { }
  2. class MyException2 extends Exception {
  3. public MyException2(String message) {
  4. super(message);
  5. }
  6. }
  7. public class Except {
  8. public static void show(int flag) throws MyException1, MyException2 {
  9. if (flag == 1) {
  10. throw new MyException1();
  11. }
  12. if (flag == 2) {
  13. MyException2 e = new MyException2("自定义类型");
  14. throw e;
  15. }
  16. }
  17. public static void main(String[] args) { // 声明异常
  18. try {
  19. show(2);
  20. } catch (MyException1 e) {
  21. e.printStackTrace();
  22. } catch (MyException2 e) { // 匹配成功
  23. e.printStackTrace(System.out); // 将异常输出到标准输出流中,即打印到主控台。
  24. } catch (Exception e) { // 父类异常必须在子类异常的后面!
  25. e.printStackTrace(System.out);
  26. }
  27. }
  28. } /* Output:
  29. MyException2: 自定义类型
  30. at Except.show(Except.java:15)
  31. at Except.main(Except.java:22)
  32. *///:~

多catch时,父类异常的catch放在最下面,否则在捕获子类异常时会编译失败。

编译器报错信息:“Unreachable catch block for xxException. It is already handled by the catch block for Exception”

通过捕获异常类型的基类Exception就可以捕获所有异常。必须放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常给捕获了。

finally代码块

无论try代码块中的异常是否抛出,都会被执行到。

  1. try {
  2. // The guarded region: Dangerous activities
  3. // that might throw A, B or C
  4. } catch (A a1) {
  5. // Handler for situation A
  6. } catch (B b2) {
  7. // Handler for situation B
  8. } catch (C c3) {
  9. // Handler for situation C
  10. } finally {
  11. // Activities that happen every time
  12. }

为了证明finally子句总能运行,可以试试下面这个程序:

  1. class ThreeException extends Exception { }
  2. public class FinallyWorks {
  3. static int count = 0;
  4. public static void main(String[] args) {
  5. while (true) {
  6. try {
  7. // Post-increment is zero first time:
  8. if (count++ == 0)
  9. throw new ThreeException();
  10. System.out.println("No exception"); // 如果抛出异常,不会被执行
  11. } catch (ThreeException e) {
  12. System.out.println("ThreeException");
  13. } finally {
  14. System.out.println("In finally clause");
  15. if (count == 2)
  16. break; // out of "while"
  17. }
  18. }
  19. }
  20. } /* Output:
  21. ThreeException
  22. In finally clause
  23. No exception
  24. In finally clause
  25. *///:~

要把除内存之外的资源恢复到它们的初始状态时,就要用到finally语句。

举个简单的例子,一般出门之前要把房间里所有的灯都关掉,可是你有时候出门会忘记关灯,这就造成了资源的浪费。所以无论怎么样都应该关掉灯,这时,这个关灯的动作可以放在finally代码块中。

  1. class OnOffException1 extends Exception { }
  2. class OnOffException2 extends Exception { }
  3. class Switch {
  4. private boolean state = false;
  5. public void on() {
  6. state = true;
  7. System.out.println(this);
  8. }
  9. public void off() {
  10. state = false;
  11. System.out.println(this);
  12. }
  13. public String toString() {
  14. return state ? "on" : "off";
  15. }
  16. }
  17. public class Final {
  18. private static Switch sw = new Switch();
  19. public static void f() throws OnOffException1, OnOffException2 { }
  20. public static void main(String[] args) throws OnOffException1, OnOffException2 {
  21. try {
  22. sw.on();
  23. f(); // Code that can throw exceptions..
  24. } finally {
  25. sw.off(); // 当异常发生时,我们无法处理,只能向上抛出,但是我们必须要关闭资源
  26. }
  27. }
  28. } /* Output:
  29. on
  30. off
  31. *///:~

return中使用finally,即使执行到returnfinally中的代码仍然会被执行。

  1. public class MultipleReturns {
  2. public static void f(int i) {
  3. System.out.println("Initialization that requires cleanup");
  4. try {
  5. System.out.println("Point 1");
  6. if (i == 1)
  7. return;
  8. System.out.println("Point 2");
  9. if (i == 2)
  10. return;
  11. System.out.println("End");
  12. return;
  13. } finally {
  14. System.out.println("Performing cleanup"); // 总是会被执行的
  15. }
  16. }
  17. public static void main(String[] args) {
  18. for (int i = 1; i <= 3; i++)
  19. f(i);
  20. }
  21. } /* Output:
  22. Initialization that requires cleanup
  23. Point 1
  24. Performing cleanup
  25. Initialization that requires cleanup
  26. Point 1
  27. Point 2
  28. Performing cleanup
  29. Initialization that requires cleanup
  30. Point 1
  31. Point 2
  32. End
  33. Performing cleanup
  34. *///:~

如果使用了退出虚拟机"System.exit(0);",finally代码块不执行。

  1. public class Exit {
  2. public static void main(String[] args) {
  3. try {
  4. System.out.println("start");
  5. // return是回到上一层,而System.exit(status)是回到最上层
  6. System.exit(0); // 正常退出程序
  7. System.out.println("---"); // 不会有报错,但是不会被执行
  8. } finally {
  9. System.out.println("exit"); // 不会执行!
  10. }
  11. }
  12. } /* Output:
  13. start
  14. *///:~

详解return

如果try或者catch语句里有return,那么代码的行为如下:


异常的限制

当覆盖方法的时候,子类声明的异常必须是父类声明的异常或异常的导出类型。

其实上面这段话很容易理解,Java之所以这样做,其实是为了保证对象的可替换性

在多态的情况下,一个后期绑定的方法声明throws了会抛出异常,在子类对象使用该方法的时候一旦抛出了异常,异常一定为子类所定义的。而在编译期对异常的处理都是以父类中该方法声明的抛出异常做出预先的处理方式,如果子类抛出的异常比父类的异常大或者为其他异常,那么将无法处理该异常。此时既无法声明异常又无法捕获异常,这种情况是编译器所不允许的。


Exception类型的方法

通过查看API发现Exception类型没有自己的方法,都是从其父类Throwable继承过来的方法。这里将介绍一下以上并没有出现的方法。

fillInStackTrace()方法

  1. public class Except {
  2. public static void show() throws Exception {
  3. throw new Exception("fillInStackTrace()方法");
  4. }
  5. public static void main(String[] args) throws Exception {
  6. try {
  7. show();
  8. } catch (Exception e) {
  9. // throw e;
  10. throw (Exception) e.fillInStackTrace(); // 变成了异常的新发生地
  11. }
  12. }
  13. } /* Output:
  14. Exception in thread "main" java.lang.Exception: fillInStackTrace()方法
  15. at Except.main(Except.java:11)
  16. *///:~

getCause()与initCause()方法

  1. class MyException extends Exception {
  2. public MyException(String message) {
  3. super(message);
  4. }
  5. }
  6. public class Except {
  7. public static void show() throws Exception {
  8. MyException e = new MyException("包装异常");
  9. throw new Exception(e); //包装异常
  10. //throw (Exception)new Exception().initCause(new MyException("包装异常")); // 等价于上面两句,构造器包装异常更为常用
  11. }
  12. public static void main(String[] args){
  13. try {
  14. show();
  15. } catch (Exception e) {
  16. System.out.println(e.getCause());
  17. }
  18. }
  19. } /* Output:
  20. MyException: 包装异常
  21. *///:~

把“被检查的异常”转换为“不检查的异常”

通过重抛异常的方式,把“被检查的异常”包装进RuntimeException里面。

  1. try {
  2. // ... to do something useful
  3. } catch (IDontKnowWhatToDoWithThisCheckedException e) {
  4. throw new RuntimeException(e);
  5. }

这些被包装的异常就成为了异常的cause,可以通过getCause()方法获取被包装的那个原始异常。

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