• 初识Java 13-2 异常


    目录

    标准Java异常

    新特性:更好的NullPointerException报告机制

    使用finally执行清理

    finally有什么用

    在return时使用finally

    缺陷:异常丢失

    异常的约束

    构造器


    本笔记参考自: 《On Java 中文版》


    标准Java异常

            Throwable类描述了任何可能抛出异常的事物,它有两个常用的子类:

    • Error:表示编译时错误和系统错误。
    • Exception:是一个基本类型,可从任何标准的Java库方法、自定义方法及运行事故中抛出。

            很显然,作为Java程序员,我们会更加关心Exception

            对异常的理解和处理是较为重要的部分(与了解各种异常相比)。基本的思路是,异常的名字代表着所发生的问题。因此,异常的名字应该是浅显易懂的。

        异常并不全部来自java.lang。有些异常是为不同的库设计的,这一点可以从他们的全限定类名或基类看出。

    特例:RuntimeException(非检查型异常)

            有这么一组异常,它们总是会被Java自动抛出,而不需要程序员将它们包含在任何异常说明之中。这种异常都被放在了RuntimeException这一基类之下。

        这就是一个继承的完美示例。

    ||| RuntimeException:表示编程错误,它包括:

    • 无法预料的错误,比如在我们控制之外的null引用。
    • 作为程序员,应该在代码中检查的错误(例如ArrayIndexOutOfBoundsException,这表示我们的数组大小可能出现问题)。

            注意:在一个地方发现的异常,往往也可能成为另一个地方的问题。

            正如之前所说,RuntimeException及其子类不需要异常说明,这类异常也被称为“非检查型异常(它们指出的是bug。若必须检查,代码会变得十分复杂)。不过,尽管我们一般不会捕捉RuntimeException,但我们可以有选择地抛出一个RuntimeException

            若不捕捉这类异常,RuntimeException就可能逐层返回,知道到达main()方法:

    【例子:若不捕捉RuntimeException

    1. public class NeverCaught {
    2. static void f() {
    3. throw new RuntimeException("来自f()");
    4. }
    5. static void g() {
    6. f();
    7. }
    8. public static void main(String[] args) {
    9. g();
    10. }
    11. }

            程序执行的结果是:

            若一个RuntimeException没有被捕获,关于这个异常的printStackTrace()会在程序结束时被调用。这就体现出了RuntimeException(及其子类)的特殊性,这类异常不会被要求写入异常说明,它们的输出将被报告给System.err

        只有RuntimeException(及其子类)类型的异常可被忽略,编译器会强制实施对所有检查型异常的处理。

    新特性:更好的NullPointerException报告机制

            在JDK 15之前,NullPointerException能够报告的信息并不多。

    【例子:NullPointerException的报错】

    1. package exceptions;
    2. class A {
    3. String s;
    4. A(String s) {
    5. this.s = s;
    6. }
    7. }
    8. class B {
    9. A a;
    10. B(A a) {
    11. this.a = a;
    12. }
    13. }
    14. class C {
    15. B b;
    16. C(B b) {
    17. this.b = b;
    18. }
    19. }
    20. public class BetterNullPointerReports {
    21. public static void main(String[] args) {
    22. C[] ca = {
    23. new C(new B(new A(null))),
    24. new C(new B(null)),
    25. new C(null),
    26. };
    27. for (C c : ca) {
    28. try {
    29. System.out.println(c.b.a.s);
    30. } catch (NullPointerException npe) {
    31. System.out.println(npe);
    32. }
    33. }
    34. }
    35. }

            若是在JDK 11的环境中运行,会得到如下的结果:

            但若是在JDK 15或更高版本中运行,则会显示:

    更多的信息有助于我们理解和处理这些异常。

    使用finally执行清理

            我们可能会需要手动进行一些清理操作,这种操作通常是内存恢复之外的动作,因为内存会由垃圾收集器处理。在这种情况下,我们可能会希望:无论try块是否抛出异常,都会有一段代码必须执行。

            为此,可以在所有的异常处理程序的末尾添加一个finally子句:

    1. try {
    2. // 被守护区域
    3. } catch(A a1) {
    4. // 情况A的处理程序
    5. } catch(B b1) {
    6. // 情况B的处理程序
    7. } finally {
    8. // 不管哪种情况都会执行的活动
    9. }

    【例子:添加了finally的异常处理】

    1. class ThreeException extends Exception {
    2. }
    3. public class FinallyWorks {
    4. static int count = 0;
    5. public static void main(String[] args) {
    6. while (true) {
    7. try {
    8. if (count++ == 0) // 使用后缀++,第一次的结果就是0
    9. throw new ThreeException();
    10. System.out.println("没有异常");
    11. } catch (ThreeException e) {
    12. System.out.println("ThreeException");
    13. } finally {
    14. System.out.println("在finally子句中");
    15. if (count == 2) // 跳出循环
    16. break;
    17. }
    18. }
    19. }
    20. }

            程序执行的结果是:

            有输出可知,无论是否抛出异常,finally子句都会执行。并且,Java中的异常不会允许我们回退到异常抛出的地方。

    finally有什么用

            若语言没有垃圾收集,并且不会自动调用析构函数,那么finally就需要确保内存的释放。但Java并不需要这一功能。对Java而言,finally主要用于清理内存之外的某些东西。

            为了演示finally的作用,首先创建一个需要使用的组件:

    1. public class PnOffSwitch {
    2. private static Switch sw = new Switch();
    3. public static void f()
    4. throws OnOffException1, OnOffException2 {
    5. }
    6. public static void main(String[] args) {
    7. try {
    8. sw.on();
    9. // 可能抛出异常的代码...
    10. f();
    11. sw.off();
    12. } catch (OnOffException1 e) {
    13. System.out.println("OnOffException1");
    14. sw.off();
    15. } catch (OnOffException2 e) {
    16. System.out.println("OnOffException1");
    17. sw.off();
    18. }
    19. }
    20. }

            两个需要使用的异常:

            若不使用finally,一般情况下我们会这样进行异常处理:

    1. public class OnOffSwitch {
    2. private static Switch sw = new Switch();
    3. public static void f()
    4. throws OnOffException1, OnOffException2 {
    5. }
    6. public static void main(String[] args) {
    7. try {
    8. sw.on();
    9. // 可能抛出异常的代码...
    10. f();
    11. sw.off();
    12. } catch (OnOffException1 e) {
    13. System.out.println("OnOffException1");
    14. sw.off();
    15. } catch (OnOffException2 e) {
    16. System.out.println("OnOffException1");
    17. sw.off();
    18. }
    19. }
    20. }

            当程序结束时,我们要求sw处于关闭状态。因此每个catch子句的末尾都添加了sw.off()。但程序还有可能抛出某个没有在这里被捕获的异常,这种情况会导致sw.off()被忽略。因此finally就有了用武之地,可以把try块中的清理工作都放在这里:

    【例子:finally的清理】

    1. public class WithFinally {
    2. static Switch sw = new Switch();
    3. public static void main(String[] args) {
    4. try {
    5. sw.on();
    6. // 可能抛出异常的代码...
    7. OnOffSwitch.f();
    8. } catch (OnOffException1 e) {
    9. System.out.println("OnOffException1");
    10. } catch (OnOffException2 e) {
    11. System.out.println("OnOffException2");
    12. } finally {
    13. sw.off(); // 无论何种情况都会执行
    14. }
    15. }
    16. }

            即使抛出的异常没有在当前这组catch子句中捕获,在异常处理机制向更高一层进行搜索之前,finally也会执行。

    【例子:总是执行的finally

    1. class FourException extends Exception {
    2. }
    3. public class AlwaysFinally {
    4. public static void main(String[] args) {
    5. System.out.println("进入第一个try块");
    6. try {
    7. System.out.println("进入第二个try块");
    8. try {
    9. throw new FourException();
    10. } finally {
    11. System.out.println("finally在第二个try块中执行");
    12. }
    13. } catch (FourException e) {
    14. System.out.println("在第一个try块的处理程序中捕获异常FourException");
    15. } finally {
    16. System.out.println("finally在第一个try块中执行");
    17. }
    18. }
    19. }

            程序执行的结果是:

        涉及breakcontinue语句时,finally也会执行。


    return时使用finally

            利用finally子句总会执行的特性,我们可以在一个多返回的方法中保证重要的清理工作能够完成:

    【例子:在多返回的方法中使用finally

    1. public class MultipleReturns {
    2. public static void f(int i) {
    3. System.out.println("初始化后执行清理");
    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("Point 3");
    12. if (i == 3)
    13. return;
    14. System.out.println("结束");
    15. return;
    16. } finally {
    17. System.out.println("执行清理");
    18. }
    19. }
    20. public static void main(String[] args) {
    21. for (int i = 1; i <= 4; i++){
    22. f(i);
    23. System.out.println();
    24. }
    25. }
    26. }

            程序执行的结果是:


    缺陷:异常丢失

            在一些特殊的finally子句的使用中,可能发生异常丢失的情况:

    【例子:特殊子句中的异常丢失】

    1. class VeryImportantException extends Exception {
    2. @Override
    3. public String toString() {
    4. return "这是一个很重要的异常,它不应该被忽略";
    5. }
    6. }
    7. class HoHumException extends Exception {
    8. @Override
    9. public String toString() {
    10. return "一个不重要的异常";
    11. }
    12. }
    13. public class LostMessage {
    14. void f() throws VeryImportantException {
    15. throw new VeryImportantException();
    16. }
    17. void dispose() throws HoHumException {
    18. throw new HoHumException();
    19. }
    20. public static void main(String[] args) {
    21. try {
    22. LostMessage lm = new LostMessage();
    23. try {
    24. lm.f();
    25. } finally {
    26. lm.dispose();
    27. }
    28. } catch (VeryImportantException |
    29. HoHumException e) {
    30. System.out.println(e);
    31. }
    32. }
    33. }

            程序执行的结果是:

            原本应该被捕获的VeryImportantExceptionfinally子句中的HoHumException取代了。这是十分严重的问题,因为它意味着一个异常可能会完全丢失(并且难以察觉)。

        C++会将在第一个异常处理前抛出第二个异常视为严重的编程错误。

            目前Java还未修复这一问题。为了处理这一麻烦,我们需要将任何可能抛出异常的方法(就是上面的dispose)包在另一个try-catch子句中。

            还有一种会丢失异常的方式,就是在finally子句中执行return

    【例子:另一种异常丢失】

    1. public class ExceptionSilencer {
    2. public static void main(String[] args) {
    3. try {
    4. throw new RuntimeException();
    5. }finally {
    6. return; // 在finally中使用return,会阻止任何的异常报错
    7. }
    8. }
    9. }

            若运行程序,会发现并无任何报错

    异常的约束

            存在着这样一个约束:在重写一个方法时,只能抛出 ①该方法的基类版本中说明的异常,或者是 ②以原有异常为基类派生而出的异常。

        这一约束指向一个概念:能够配合基类工作的代码,可以自动配合从这个基类派生出的其他类的对象进行工作,异常也不会例外。

    【例子:异常的各种约束】

    1. class BaseballException extends Exception {
    2. }
    3. class Foul extends BaseballException {
    4. }
    5. class Strike extends BaseballException {
    6. }
    7. abstract class Inning {
    8. Inning() throws BaseballException {
    9. }
    10. public void event() throws BaseballException {
    11. }
    12. public abstract void atBat() throws Strike, Foul;
    13. public void walk() { // 该方法没有抛出检查型异常
    14. }
    15. }
    16. class StormException extends Exception {
    17. }
    18. class RainedOut extends StormException {
    19. }
    20. class PopFoul extends Foul {
    21. }
    22. interface Storm {
    23. void event() throws RainedOut;
    24. void rainHard() throws RainedOut;
    25. }
    26. public class StormyInning extends Inning implements Storm {
    27. // 子类构造器可以有新的异常,但在处理这些异常时还需要考虑基类的异常
    28. public StormyInning()
    29. throws RainedOut, BaseballException {
    30. }
    31. public StormyInning(String s)
    32. throws BaseballException {
    33. }
    34. // 普通的方法在重写时,必须遵守基类方法的约定
    35. // 1. 访问权限
    36. // 2. 不能擅自添加异常
    37. // void walk() throws PopFoul{}
    38. // event()方法已经存在于基类当中,接口无法增加其的异常
    39. // public void event() throws RainedOut {}
    40. // 若是基类中不存在的方法,则可以自行添加声明:
    41. @Override
    42. public void rainHard() throws RainedOut {
    43. }
    44. // 即使基类版本会抛出异常,其子类版本也可以选择不进行异常抛出:
    45. @Override
    46. public void event() {
    47. }
    48. // 若是重写的方法,可以抛出其基类版本所说明的异常的子类:
    49. @Override
    50. public void atBat() throws PopFoul { // PopFoul是Foul的子类
    51. }
    52. public static void main(String[] args) {
    53. try {
    54. StormyInning si = new StormyInning();
    55. si.atBat();
    56. } catch (PopFoul e) {
    57. System.out.println("Pop Foul(一次违规的挥棒)");
    58. } catch (RainedOut e) {
    59. System.out.println("Rained out(下雨了)");
    60. } catch (BaseballException e) { // 这里,派生的si.atBat()不会抛出Strike异常
    61. System.out.println("通用的baseball(棒球)异常");
    62. }
    63. try {
    64. // 若向上转型,情况会有所不同
    65. Inning i = new StormyInning();
    66. i.atBat();
    67. // 此时,就必须捕获来自基类版本的异常
    68. } catch (Strike e) {
    69. System.out.println("Strike(发生碰撞)");
    70. } catch (Foul e) {
    71. System.out.println("Foul(犯规)");
    72. } catch (RainedOut e) {
    73. System.out.println("Rained out(下雨了)");
    74. } catch (BaseballException e) {
    75. System.out.println("通用的baseball(棒球)异常");
    76. }
    77. }
    78. }

            先观察Inning类:

            该类的构造器和event()方法都有异常列表,这就向编译器说明它们会抛出异常,但实际上并没有。这种做法是合法的,因为编译器会要求用户捕获任何可能在event()的重写版本中添加的异常(这也适用于抽象方法)。

            StormyInning继承了Inning类和Storm接口,其中event()方法即存在于Inning中,也存在于Storm中。注意,这个event()方法不能改变Inning中的event()方法的异常说明:

    假设这种语法能够成立,那么在我们使用基类的时候,就难以判断是否捕获了正确的异常。

        构造器由于其多样的调用形式,在异常的约束方面也不同于一般的方法。有上述例子可以发现,构造器可以抛出任何异常。也因此,子类构造器必须在其异常说明中声明基类构造器提到的异常

            子类构造器不能捕获基类构造器抛出的异常。

            再看StormyInning类中的walk()方法:

    这个方法之所以无法编译,就是因为其抛出了一个Inning.walk()没有抛出的异常。

            而从StormyInning类中的event()方法中可以发现:

    即使基类方法抛出异常,其子类也可以选择不进行异常抛出。因为这不会破坏基类版本会抛出异常的情况。

            最后需要提一点,在main()中,我们首先定义了一个StormInning对象:

    StormyInning si = new StormyInning();

    此时,编译器会强制要求我们处理StormInning类声明会抛出的异常。但是,若我们将其向上转型为Inning

    Inning i = new StormyInning();

    则编译器会强制我们捕获基类声明会抛出的异常。

        异常说明不是方法类型的一部分,因此不能依赖方法说明作为重载方法的依据。

            注意:在继承和重写的过程中,“异常说明”可以缩小,但是不能扩大——这就和继承过程中的规则恰恰相反。

    构造器

            我们需要常常怀疑:当发生异常时,程序是否正确地进行了清理。

            构造器的存在会使得对象有一个安全的起始状态。但是,构造器可能会执行某些操作,比如打开文件,这种操作需要通过特殊的清理方法处理。若构造器内抛出了异常,这些清理行为可能就不会正确执行了。

            finally可能是一个办法。但是,由于finally每次都会执行清理代码,若构造器半途而废,finally就可能会把构造器还未成功构建的部分也一并清理。

    【例子:文件的打开与清理】

    1. import java.io.BufferedReader;
    2. import java.io.FileNotFoundException;
    3. import java.io.FileReader;
    4. import java.io.IOException;
    5. public class InputFile {
    6. private BufferedReader in;
    7. public InputFile(String fname) throws Exception {
    8. try {
    9. in = new BufferedReader(new FileReader(fname));
    10. // 剩下的部分也可能抛出异常
    11. } catch (FileNotFoundException e) {
    12. System.out.println("无法打开[" + fname + "]");
    13. // 因为没有打开,所以不用关闭文件
    14. throw e;
    15. } catch (Exception e) {
    16. // 除上述异常,其他异常情况都需要关闭文件
    17. try {
    18. in.close(); // 对于可能抛出异常的close()方法,也需要使用try进行处理
    19. } catch (IOException e2) {
    20. System.out.println("in.close() 执行失败");
    21. }
    22. throw e; // 重新抛出异常
    23. } finally {
    24. // 不应该在这里进行文件关闭
    25. }
    26. }
    27. public String getLine() {
    28. String s;
    29. try {
    30. s = in.readLine();
    31. } catch (IOException e) { // 在方法内部直接处理异常
    32. throw new RuntimeException("readline() 失败");
    33. }
    34. return s;
    35. }
    36. public void dispose() {
    37. try {
    38. in.close();
    39. System.out.println("dispose() 执行成功");
    40. } catch (IOException e) {
    41. throw new RuntimeException("in.close() 失败");
    42. }
    43. }
    44. }

            除了捕获FileNotFoundException(文件未发现)的catch子句外,其他catch子句都应该关闭文件(此时文件已经被打开)。

            比较好的做法是,在执行完异常处理后再次抛出异常。因为构造器的调用已经失败,那么我们当然不会希望构造器的调用者认为构造器顺利完成了任务。

            就像之前所说,在构造器中,finally绝对不适合调用close()来进行文件关闭。因为这种做法会导致文件在构造器调用完毕后就被关闭,这很明显与我们的期望相违背。

        上述的代码中,有些方法会抛出异常,而有的方法会在内部直接处理异常。这种对异常处理的设计需要仔细考虑。

            现在必须提醒一点,Java存在着一个缺点:除了内存的清理,其他清理都不会自动发生。这就导致Java必须告诉客户程序员,那些要让他们自己处理。

            下面的例子演示了如何清理可能抛出异常的类:

    【例子:嵌套的try块】

    1. public class Cleanup {
    2. public static void main(String[] args) {
    3. try {
    4. InputFile in = new InputFile("Cleanup.java");
    5. try {
    6. String s;
    7. int i = 1;
    8. while ((s = in.getLine()) != null)
    9. ; // 一行一行进行数据处理
    10. } catch (Exception e) {
    11. System.out.println("在main()中捕获异常");
    12. e.printStackTrace(System.out);
    13. } finally {
    14. in.dispose();
    15. }
    16. } catch (Exception e) {
    17. System.out.println("InputFile对象构造失败");
    18. }
    19. }
    20. }

            程序执行的结果是:

            InputFile对象的构造存在于外层的try块之中。若构造器执行失败,程序不会向下进入下一个try块中,而是直接来到外层的try块对应的catch子句内。

        这里体现了一种清理惯用法:在创建了一个需要清理的对象后,直接跟一个try-finally块。

    【例子:清理惯用法】

    1. class NeedsCleanup {
    2. // 该构造不会失败
    3. private static long counter = 1;
    4. private final long id = counter++;
    5. public void dispose() {
    6. System.out.println("需要清理 id:" + id);
    7. }
    8. }
    9. class ConstructionException extends Exception {
    10. }
    11. class NeedsCleanup2 extends NeedsCleanup {
    12. // 该构造可能失败
    13. NeedsCleanup2() throws ConstructionException {
    14. }
    15. }
    16. public class CleanupIdiom {
    17. public static void main(String[] args) {
    18. // 直接的处理方式:在需要清理的对象后紧跟一个try-finally块
    19. NeedsCleanup nc1 = new NeedsCleanup();
    20. try {
    21. // ...
    22. } finally {
    23. nc1.dispose();
    24. }
    25. // 可以将不会失败的构造对象组织在一起:
    26. NeedsCleanup nc2 = new NeedsCleanup();
    27. NeedsCleanup nc3 = new NeedsCleanup();
    28. try {
    29. // ...
    30. } finally {
    31. // 释放顺序与构造顺序相反
    32. nc3.dispose();
    33. nc2.dispose();
    34. }
    35. // 若构造可能失败,就需要确保每个对象的清理
    36. try {
    37. NeedsCleanup2 nc4 = new NeedsCleanup2();
    38. try {
    39. NeedsCleanup2 nc5 = new NeedsCleanup2();
    40. try {
    41. // ...
    42. } finally {
    43. nc5.dispose();
    44. }
    45. } catch (ConstructionException e) {
    46. // 处理nc5可能报出的异常
    47. System.out.println(e);
    48. } finally {
    49. nc4.dispose();
    50. }
    51. } catch (ConstructionException e) {
    52. // 处理nc4
    53. System.out.println(e);
    54. }
    55. }
    56. }

            程序执行的结果是:

            在nc4nc5的构造处理的过程中,可以发现try-catch导致的麻烦:为了处理每一个对象,我们需要为它们分别设定try-catch的处理。

        为了处理异常,我们需要尽可能考虑所有可能性,并确保它们能被处理。

  • 相关阅读:
    异步时钟无毛刺切换的波形演示
    洛谷P1084 树上问题,思维,贪心,二分答案
    打造人脸磨皮算法新标杆,满足企业多元化需求
    基于MATLAB的高阶(两个二阶级联构成的四阶以及更高阶)数字图形音频均衡器系数计算(可直接用于DSP实现)
    Spring Boot中的微信支付(小程序)
    【Python第三方包】使用Python的Translate包进行文本翻译
    Anaconda下安装Jupyter notebook
    Windows内核--为什么C语言适合编写内核?(1.2)
    Callable、Future和FutureTask
    【动态规划】输出所有的最长递增子序列和字典序最小的
  • 原文地址:https://blog.csdn.net/w_pab/article/details/133681600