从 0 开始学 Java——抛出和声明异常的代码实现
抛出和声明异常
概述我们在编写代码时,有时候因为某些原因,并不想在这个方法中立即处理产生的异常,也就是说并不想进行异常的捕获。那么此时我们可以把这些异常先进行声明和抛出,即在这个方法中暂时不处理,而是扔给别人去处理。就好比你发现有个小孩犯了错,但你不是这个小孩的监护人,你来批评处理这个小孩子就不太合适,所以可以把他抓住扔给其父母,让他的父母来处理这个错误。
这就是所谓的异常传播机制:当某个方法抛出了异常,如果当前方法没有捕获该异常,该异常就会被抛到更上层的调用方法,逐层传递,直到遇到某个 try ... catch 被捕获为止。
异常的传播,在 Java 中主要是用声明和抛出异常的关键字来实现,分别是 throws 和 throw。我们可以使用 throws 关键字在方法上声明本方法要拋出的异常,使用 throw 关键字拋出某个异常对象。接下来我就给大家详细介绍该如何声明异常和拋出异常。
throw 抛出异常 2.1 基本语法当代码中发生了异常,程序会自动抛出一个异常对象,该对象包含了异常的类型和相关信息。但是如果我们想主动抛出异常,则可以使用 throw 关键字来实现。
java 复制代码 throw 某个 Exception 类;这里的某个 Exception 类必须是 Throwable 类或其子类对象。如果是自定义的异常类,也必须是 Throwable 的直接或间接子类,否则会发生错误。
2.2 代码实现接下来我给大家设计一个案例,来演示 throw 的使用:
java 复制代码 public class Demo04 {// throw 的使用 public static void myMethod(boolean flag) throws Exception {if (flag) {//当 flag 为 true 时就抛出一个 Exception 对象 throw new Exception("主动抛出来的异常对象");}}
}在上面这个案例中,myMethod 方法产生了异常,但是自己却没有处理,而是进行了抛出。那么会抛给谁呢?我们看到,此时 caller 方法调用了 myMethod 方法,即 caller 方法是 myMethod 方法的上层调用者,所以 myMethod 方法中产生的异常就抛给了 caller 方法。但是如果在 caller 方法中对这个异常也没有进行处理,caller 方法也会把这个异常继续向上层传递。这样逐层向上抛出异常,直到最外层的异常处理程序终止程序并打印出调用栈才会结束。我们来看看异常信息栈的打印结果:
从上面的异常栈信息中可以看出,Exception 是在 myMethod 方法中被抛出的,从下往上看,调用层次依次是:
main()调用 caller(); caller()调用 myMethod(); myMethod()抛出异常;
而且每层的调用都给出了源代码的行号,我们可以直接定位到产生异常的代码行,这样我们就可以根据异常信息栈进行异常调试。尤其是要注意,如果异常信息栈中有“Caused by: Xxx”这样的信息,说明我们捕获到了造成问题的根源。但是有时候异常信息中可能并没有“Caused by”这样的信息,我们可以在代码中使用 Throwable 类的 getCause()方法来获取原始异常,此时如果返回了 null,说明已经是“根异常”了。这样有了完整的异常栈的信息,我们才能快速定位并修复代码的问题。另外我们还要注意,throw 关键字不会单独使用,它的使用要符合异常的处理机制。我们一般不会主动抛出一个新的异常对象,而是应该避免异常的产生。
2.3 在 try 或 catch 中抛出异常有些同学很善于思考,于是就提出了问题:如果我们在 try 或者 catch 语句块中抛出一个异常,那么 finally 语句还能不能执行? 为了给大家解答清楚这个问题,我再给大家设计一个案例。
java 复制代码 public static void main(String[] args) {try {int a=100;System.out.println("a="+a);} catch (Exception e) {System.out.println("执行 catch 代码,异常信息:"+e.getMessage());//在 catch 中抛出一个新的运行时异常 throw new RuntimeException(e);} finally {System.out.println("非特殊情况,一定会执行 finally 里的代码");}}我们直接来看上述代码的执行结果:
从上面的结果中可以看出,JVM 会先进入到 catch 代码块中进行异常的正常处理,如果发现在 catch 中也产生了异常,则会进入到 finally 中执行,finally 执行完毕后,会再把 catch 中的异常抛出。所以即使我们在 try 或 catch 中抛出了异常,也并不会影响 finally 的执行。
2.4 异常屏蔽但是这时有的小伙伴又提问了,如果我们在执行 finally 语句中也抛出一个异常,又会怎么样呢?所以我继续给大家进行论证,我们对上面的案例进行适当改造,在 finally 中也抛出一个异常。
java 复制代码 public static void main(String[] args) {try {int a=100/0;System.out.println("a="+a);} catch (Exception e) {System.out.println("执行 catch 代码,异常信息:"+e.getMessage());//在 catch 中抛出一个新的运行时异常 throw new RuntimeException(e);} finally {System.out.println("非特殊情况,一定会执行 finally 里的代码");//在 finally 中也抛出一个异常 throw new IllegalArgumentException("非法参数异常");}}我们还是直接来看看执行结果:
从上面的结果中我们可以看出,在 finally 抛出异常后,原来 catch 中抛出的异常不见了。这是因为默认情况下只能抛出一个异常,而之前那个没被抛出的异常称为“被屏蔽的异常(Suppressed Exception) ”。
2.5 获取全部异常信息但是有些较真的小伙伴就说,如果我就想知道所有的异常信息,包括 catch 中被屏蔽的那些异常信息,这该怎么办?
我们可以定义一个 Exception 的 origin 变量来保存原始的异常信息,然后调用 Throwable 对象中的 addSuppressed()方法 ,把原始的异常信息添加进来,最后在 finally 中继续 抛出,并 利用 throws 关键字抛出 Exception 。
java 复制代码 public class Demo07 {//这里要利用 throws 关键字抛出 Exception@SuppressWarnings("finally")public static void main(String[] args) throws Exception {//定义一个异常变量,存储 catch 中的异常信息 Exception origin = null; try {int a=100/0;System.out.println("a="+a);} catch (Exception e) {System.out.println("执行 catch 代码,异常信息:"+e.getMessage());//存储 catch 中的异常信息 origin = e;//抛出 catch 中的异常信息 throw e;} finally {System.out.println("非特殊情况,一定会执行 finally 里的代码");Exception e = new IllegalArgumentException();if (origin != null) {//将 catch 中的异常信息添加到 finally 中的异常信息中 e.addSuppressed(origin);}//抛出 finally 中的异常信息,注意此时需要在方法中利用 throws 关键字抛出 Exceptionthrow e;}}}此时的执行结果如下所示:
从上面的结果中我们可以看出,即使 catch 和 finally 都抛出了异常时,catch 异常被屏蔽,但我们通过 addSuppressed 方法,最终仍然获取到了完整的异常信息。但是我们也要知道,绝大多数情况下,都不应该在 finally 中抛出异常。
throws 声明异常对于方法中不想处理的异常,除了可以利用 throw 关键字进行抛出之外,还有别的办法吗?其实我们还可以在该方法的头部,利用 throws 关键字来声明这个不想处理的异常,把该异常传递到方法的外部进行处理。
3.1 基本语法 throws 关键字的基本语法如下:
java 复制代码返回值类型 methodName(参数列表) throws Exception 1,Exception2,…{…}通过这个语法,我们可以看出,在一个方法中可以利用 throws 关键字同时声明抛出多个异常:Exception 1,Exception2,… 多个异常之间利用","逗号分隔。如果被调用方法抛出的异常类型在这个异常列表中,则必须在该方法中捕获或继续向上层调用者抛出异常。而这里继续声明抛出的异常,可以是方法本身产生的异常,也可以是调用的其他方法抛出的异常。
3.2 代码实现为了让大家理解 throws 关键字的用法,接下来我继续设计一个案例进行说明。
java 复制代码 import java.io.BufferedReader;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;
/**
@author 一一哥 Sun*/public class Demo08 {public static void main(String[] args) {try {//在调用 readFile 的上层方法中进行异常的捕获 readFile();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}
// throws 的用法--抛出了两个异常 public static void readFile() throws FileNotFoundException,IOException {// 定义一个缓冲流对象,以后在 IO 流阶段壹哥会细讲 BufferedReader reader = null;// 对接一个 file.txt 文件,该文件可能不存在 reader = new BufferedReader(new FileReader("file.txt"));// 读取文件中的内容。所有的 IO 流都可能会产生 IO 流异常 String line = reader.readLine();while (line != null) {System.out.println(line);line = reader.readLine();}}}在上面的案例中,我们在 readFile 方法中进行了 IO 流的操作,此时遇到了两个异常,但是我们不想在这里进行异常的捕获,就可以利用 throws 关键字进行异常的声明抛出。然后 main()方法作为调用 readFile 的上层方法,就需要对异常进行捕获。当然,如果 main 方法也不捕获这两个异常,该异常就会继续向上抛,抛给 JVM 虚拟机,由虚拟机进行处理。
但是我们在使用 throws 时要注意,子类在重写父类的方法时,如果父类的方法带有 throws 声明,子类方法声明中的 throws 异常,不能出现父类对应方法中 throws 里没有的异常类型,即子类方法拋出的异常范围不能超过父类定义的范围。也就是说,子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或同类,子类方法声明抛出的异常不能比父类方法声明抛出的异常多。
因此利用这一特性,throws 也可以用来限制子类的行为。子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
3.3 throws 执行逻辑根据上面的案例执行结果,我给大家总结一下 throws 关键字声明抛出异常的执行逻辑是:
如果当前方法不知道如何处理某些异常,该异常可以交由更上一级的调用者来处理,比如 main()方法; 如果 main()方法不知道该如何处理该异常,也可以使用 throws 关键字继续声明抛出,该异常将交给 JVM 去处理; 最终 JVM 会打印出异常的跟踪栈信息,并中止程序运行,这也是程序在遇到异常后自动结束的原因。
3.4 注意事项我们在使用 throws 时也要注意如下这些事项:
只能在方法的定义签名处声明可能抛出的异常类型,否则编译器会报错; 如果一个方法声明了抛出异常,但却没有在上层的方法体中对抛出的异常进行处理或继续抛出该异常,编译器会报错; throws 关键字只是声明方法可能抛出的异常类型,它并不一定真的会抛出异常; 如果一个方法中可能会有多个异常抛出,可以使用逗号将它们分隔,如 throws Exception1, Exception2, Exception3 等; 子类方法拋出的异常范围不能超过父类定义的范围。
throw 与 throws 的区别由于 throw 和 throws 长得特别像,功能也有类似之处,为了不让大家产生迷惑,所以我给大家总结一下 throw 和 throws 的区别:
throw 关键字用来抛出一个特定的异常对象,可以使用 throw 关键字手动抛出异常,执行 throw 一定会抛出某种异常对象; throws 关键字用于声明 一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常 ; throw 需要用户自己捕获相关的异常,再对其进行相关包装,最后将包装后的异常信息抛出; throws 通常不必显示地捕获异常,可以由系统自动将所有捕获的异常信息抛给上层方法; 我们通常在方法或类定义时,通过 throws 关键字声明该方法或类可能拋出的异常信息,而在方法或类的内部通过 throw 关键字声明一个具体的异常信息。
结语至此,今天的内容讲解完毕了。希望可以多多练习,才能尽快掌握哦。
评论