• 第七章《Java的异常处理》第4节:throw与throws关键字


    在Java的异常处理系统中,除了前面学过的try、catch和finally关键字之外,还有两个非常重要的关键字,它们分别是throw和throws。这两个关键字之间只相差一个字母,但意义却相差很多。

    7.4.1throw关键字的使用

    7.3小节曾经讲过:当有内外两层catch时,如果内层catch捕获到异常,那么外层catch就不会重复捕获。如果内层catch捕获到异常后,希望交给外层catch进行处理,那么它就必须把这个异常对象传递给外层catch。如何把异常对象传递到外层catch呢?Java语言中,有一个throw关键字能够完成这个操作。throw这个单词的中文意思是“抛出”,在实际开发中,它能把异常抛给更外层的catch,进而可以让更外层的catch捕获并处理这个异常。以下的【例07_10】就能很好的展示throw关键字的作用。

    【例07_10 使用throw关键字抛出异常】

    Exam07_10.java

    1. public class Exam07_10 {
    2.     public static void main(String[] args) {
    3.         int a;
    4.         try {
    5.             try {
    6.                 a = 1/0;//①
    7.             }catch(ArithmeticException e) {//内层catch
    8.                 System.out.println("内层catch捕获异常");
    9.                 throw e;//抛出已捕获的异常
    10.             }
    11.         }catch(Exception e) {//外层catch
    12.             System.out.println("外层catch捕获异常");
    13.         }
    14.     }
    15. }

    在【例07_10】中,执行语句①时会产生算术异常,此时内层catch捕获到这个异常并输出“内层catch捕获异常”,但随后通过语句“throw e;”又把该异常抛向更外层。这个异常虽然是从内层catch中抛出的,但对于外层catch而言,异常仍然是来自于外层try,因此外层catch能够捕获到这个异常,所以会输出“外层catch捕获异常”。【例07_10】的运行结果如图7-11所示。

    图7-11 【例07_10】运行结果

    很多读者不明白为什么在捕获异常之后还要再次抛出它。大家知道:异常对象中封装了某次运行错误的详细信息,有时为了能够把这些错误信息向更外层传递,所以会抛出已经捕获的异常。另外,throw关键字不仅仅能抛出已经捕获的异常对象,它还能抛出程序员自己创建的异常对象,常见的场景就是:当程序员通过判断语句发现程序运行出现不合理状态,就可以通过抛出异常的方式来通知外层这种不合理状态的存在,这种应用技巧将在7.5小节中进行讲解。

    在使用throw关键字时还需注意:并不是所有类型的对象都可以通过throw关键字抛出,Java语言仅允许抛出Throwable类及其子类的对象,如果抛出的是其他类型的对象都会导致语法错误。另外,当程序运行到throw语句时会立刻跳转到相应的catch块中,如果没有相应的catch块捕获被抛出的异常,虚拟机会中止程序运行,所以throw语句后面不能再有其他语句,因为这些语句根本没有机会被执行,如图7-12所示。

    图7-12 throw语句之后不能再有其他语句

    7.4.2throws关键字的使用

    在Java语言中,还有一个throws关键字,这个关键字只比throw关键字多了一个字母s,但它们的意义差别很大。throws关键字都写在方法的后面,用来声明这个方法在运行过程中可能会产生哪些异常,具体格式如下。

    返回值类型 方法名(参数列表) throws 异常列表{

       语句;

    }

    为什么要声明一个方法在运行时会抛出哪些异常呢?请看下面的【例07_11】:

    【例07_11 方法内部处理异常】

    Exam07_11.java

    1. public class Exam07_11 {
    2.     public static int divide(int a,int b) {
    3.         int r = 0;
    4.         try {
    5.             r = a/b;
    6.         }catch(Exception e) {
    7.             e.printStackTrace();
    8.         }
    9.         return r;
    10.     }
    11.     public static void main(String[] args) {
    12.         int x = divide(1,0);//调用divide()方法会出现异常
    13.         int y = 2*x; //用divide()方法返回值进行后续计算
    14.         System.out.println("运算结果为:"+y);
    15.     }
    16. }

    在【例07_11】的Exam07_11类中定义了一个用于做整数除法的divide()方法,当main()方法在调用这个方法时,由于传递了不合理的参数导致在divide()方法运行时产生算术异常,在divide()方法的内部对这个异常进行了处理。【例07_11】的运行结果如图7-13所示。

    图7-13 【例07_11】运行结果

    从图7-13可以看出:divide()方法在运行时产生了异常,因而方法的返回值其实是一个无效运算结果。虽然在divide()方法内部对这个异常进行了处理,但是main()方法仍然用这个无效的运算结果完成了后续的计算,这导致后续的计算结果必然是错误的。由此可以得知:divide()方法对异常进行处理并不会阻止整个程序走上错误的运行道路。如果希望在方法运行出现异常的情况下程序能停止后续的计算,就必须把处理异常的工作从divide()方法中挪到main()方法中进行。具体思路是:当在main()方法中调用divide()方法时,如果发现divide()方法运行出现异常,要立即停止后续的计算并且转入catch块中处理异常。既然要把处理异常的工作放在main()方法中进行,就要由divide()方法告知main()方法它在运行过程中可能产生哪些异常,这个告知的工作该如何完成呢?很简单,只需要在定义divide()方法时用throws关键字声明一下方法运行可能抛出的异常,具体实现过程请看下面的【例07_12】:

    【例07_12 使用throws关键字声明方法抛出的异常】

    Exam07_12.java

    1. public class Exam07_12 {
    2.     public static int divide(int a,int b) throws Exception{
    3.         int r = 0;
    4.         r = a/b;
    5.         return r;
    6.     }
    7.     public static void main(String[] args) {
    8.         try{
    9.             int x = divide(1,0);
    10.             int y = 2*x;//后续计算
    11.             System.out.println("运算结果为:"+y);
    12.         }catch(Exception e) {
    13.             System.out.println("运算出现异常!");
    14.         }
    15.     }
    16. }

    【例07_12】的代码由【例07_11】修改而来。在【例07_12】中,divide()方法对异常没有做任何处理,所有处理异常的工作都在main()方法中进行。main()方法把调用divide()方法的语句放在了try块中,并搭配了一个catch对异常进行捕获。而divide()方法只是用throws关键字声明了自身在运行过程中可能会产生哪些异常。【例07_12】的运行结果如图7-14所示。

    图7-14 【例07_12】运行结果

    从图7-14可以看出,divide()方法在运行过程中出现异常后,没有进行后续的计算,而是立即进入main()方法的catch块中对异常进行了处理,从而避免了整个程序走上错误的运行道路。

    有读者会问:如果divide()方法使用throws关键字声明了可能产生的异常,但main()方法并没有对异常进行任何处理,那么这个声明岂不是失去了意义?想要回答这个问题,必须要了解一下Java语言中异常的分类。在Java语言中,把异常分为两类,一类叫“已检查异常”,另一类叫“未检查异常”。Exception有一个子类叫做RuntimeException,RuntimeException和它的子类属于“未检查异常”,而剩余异常类均为“已检查异常”。编译器对已检查异常有强制性处理要求,也就是说:编译器要求程序员必须对可能出现的已检查异常进行处理,否则程序无法通过编译。【例07_12】中divide()方法所声明的Exception也属于已检查异常。divide()方法已经明确声明可能抛出Exception,如果不对divide()方法进行异常处理,将会出现图7-15所示的语法错误。

    图7-15 未处理已检查异常

    从图7-15可以看出,只要一个方法声明了自身可能会抛出已检查异常,该方法的调用者就必须对所声明的异常进行处理,因此throws关键字所做的声明工作不会没有意义。另外,一个方法内部如果有可能出现已检查异常,编译器会要求程序员要么在方法内部就处理这个异常,要么用throws关键字声明异常来告知调用者处理,绝对不允许“置之不理”。

    既然编译器强制要求程序员必须处理检查异常,那么读者就必须知道Java语言中有哪些常见的已检查异常。实际开发过程中,已检查异常往往出现在数据库操作、IO操作和线程操作几个领域。例如图7-7中所示的SQLException、IOException等都属于已检查异常。而算术异常ArithmeticException、数组越界异常ArrayIndexOutOfBoundsException、空指针异常NullPointerException等均属于未检查异常。为什么编译器不强制要求程序员处理未检查异常呢?就是因为未检查异常可以通过判断语句避免它的产生,例如:通过if语句就能提前判断出除数是否为0,这样完全可以避免算术异常的产生。而已检查异常很难通过程序提前判断,所以编译器要求程序员必须设置catch块来处理这些异常,即使在方法内部无法进行处理,也要用throws关键字声明它产生的可能性。

    实际开发过程中,方法使用throws关键字声明所抛出的异常都是已检查异常,不会是未检查异常,因为如果方法声明抛出的是未检查异常,那么编译器并不强制要求处理这个异常,这样会导致声明失去意义。而【例07_12】中,divide()方法在运行过程中所抛出的异常本来是ArithmeticException,它是一个未检查异常,但为了让读者理解throws关键字的作用,所以把divide()方法所声明的异常写成了被编译器强制要求处理的Exception。另外读者还必须清楚:方法使用throws关键字对异常进行声明,并不表示说该方法运行时一定会出现异常,throws关键字只是告知调用者方法运行存在产生异常的“可能性”。

    如果一个方法在运行时可能产生多种异常,可以在throws关键字的后面把每一种可能产生的异常都声明出来,各种异常之间用逗号进行分隔。Java语言还规定:子类重写父类方法时,子类方法所声明的异常范围不能超过父类的原版方法。读者不能单纯的认为声明的异常种类少就表示它的范围小,例如父类中的method()方法被定义为:

    1. public void method()throws IOException,SQLException{
    2.    ...
    3. }

    而子类中的method()方法被定义为:

    1. public void method()throws Exception{
    2.    ...
    3. }

    父类的method()方法声明了IOException和SQLException两个异常,而子类的method()方法只声明了Exception一个异常。从表明上看,子类method()方法声明的异常更少,但Exception是所有异常类的祖先,它可以表示所有的异常,因此一个Exception所表示的异常范围已经远远超过IOException和SQLException这两个异常所表示的范围,这种情况下,编译器就会提示重写方法出现了语法错误。

    除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!

  • 相关阅读:
    Grafana+Prometheus 搭建 JuiceFS 可视化监控系统
    (shorthand) pixelNeRF: Neural Radiance Fields from One or Few Images
    vue基础-动态class、动态style、vue过滤器、vue计算属性vue基础-动态class、动态style、vue过滤器、vue计算属性
    jQuery中attr()、prop()、data()用法及区别
    数据仓库: 2- 数据建模
    基于前后端分离的博客系统
    springboot多模块扫包中的@SpringBootApplication、@ComponentScan和@MapperScan问题
    编程语言参数传递方式和变量的两种基本类型
    Python 检测网络是否连通
    uni-app小程序开发踩过的坑之ref无法获取当前元素的宽高数据
  • 原文地址:https://blog.csdn.net/shalimu/article/details/128040411