likes
comments
collection
share

从零开始学Java之详解抛出和声明异常的代码实现

作者站长头像
站长
· 阅读数 12

前言

在上一篇文章中,壹哥带大家通过代码实现了异常的捕获和处理,但是异常除了捕获之外,还可以进行声明和抛出。今天壹哥会再次通过一篇文章,来教会大家该如何进行异常的抛出。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【4000】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear…

Gitee: 一一哥/从零开始学Java

一. 抛出和声明异常

1. 概述

我们在编写代码时,有时候因为某些原因,并不想在这个方法中立即处理产生的异常,也就是说并不想进行异常的捕获。那么此时我们可以把这些异常先进行声明和抛出,即在这个方法中暂时不处理,而是扔给别人去处理。就好比你发现有个小孩犯了错,但你不是这个小孩的监护人,你来批评处理这个小孩子就不太合适,所以可以把他抓住扔给其父母,让他的父母来处理这个错误。

这就是所谓的异常传播机制当某个方法抛出了异常,如果当前方法没有捕获该异常,该异常就会被抛到更上层的调用方法,逐层传递,直到遇到某个 try ... catch 被捕获为止。

异常的传播,在Java中主要是用声明和抛出异常的关键字来实现,分别是throws和throw。我们可以使用throws关键字在方法上声明本方法要拋出的异常,使用throw关键字拋出某个异常对象。接下来壹哥就给大家详细介绍该如何声明异常和拋出异常。

2. throw抛出异常

2.1 基本语法

当代码中发生了异常,程序会自动抛出一个异常对象,该对象包含了异常的类型和相关信息。但是如果我们想主动抛出异常,则可以使用throw关键字来实现。

throw 某个Exception类;

这里的某个Exception类必须是Throwable类或其子类对象。如果是自定义的异常类,也必须是Throwable的直接或间接子类,否则会发生错误。

2.2 代码实现

接下来壹哥给大家设计一个案例,来演示throw的使用:

public class Demo04 {
    // throw的使用
    public static void myMethod(boolean flag) throws Exception {
        if (flag) {
            //当flag为true时就抛出一个Exception对象
            throw new Exception("主动抛出来的异常对象");
        }
    }

    public static void caller() {
        try {
             //调用myMethod方法
             myMethod(true);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("处理主动抛出来的异常:" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        caller();
    }
}

在上面这个案例中,myMethod方法产生了异常,但是自己却没有处理,而是进行了抛出。那么会抛给谁呢?我们看到,此时caller方法调用了myMethod方法,即caller方法是myMethod方法的上层调用者,所以myMethod方法中产生的异常就抛给了caller方法。但是如果在caller方法中对这个异常也没有进行处理,caller方法也会把这个异常继续向上层传递。这样逐层向上抛出异常,直到最外层的异常处理程序终止程序并打印出调用栈才会结束。我们来看看异常信息栈的打印结果:

从零开始学Java之详解抛出和声明异常的代码实现

从上面的异常栈信息中可以看出,Exception是在myMethod方法中被抛出的,从下往上看,调用层次依次是:

  1. main()调用caller();
  2. caller()调用myMethod();
  3. myMethod()抛出异常;

而且每层的调用都给出了源代码的行号,我们可以直接定位到产生异常的代码行,这样我们就可以根据异常信息栈进行异常调试。尤其是要注意,如果异常信息栈中有“Caused by: Xxx”这样的信息,说明我们捕获到了造成问题的根源。但是有时候异常信息中可能并没有“Caused by”这样的信息,我们可以在代码中使用Throwable类的getCause()方法来获取原始异常,此时如果返回了null,说明已经是“根异常”了。这样有了完整的异常栈的信息,我们才能快速定位并修复代码的问题。 另外我们还要注意,throw关键字不会单独使用,它的使用要符合异常的处理机制。我们一般不会主动抛出一个新的异常对象,而是应该避免异常的产生。

2.3 在try或catch中抛出异常

有些同学很善于思考,于是就提出了问题:如果我们在 try 或者 catch 语句块中抛出一个异常,那么 finally 语句还能不能执行? 为了给大家解答清楚这个问题,壹哥再给大家设计一个案例。

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里的代码");
    }
}

我们直接来看上述代码的执行结果:

从零开始学Java之详解抛出和声明异常的代码实现

从上面的结果中可以看出,JVM会先进入到catch代码块中进行异常的正常处理,如果发现在catch中也产生了异常,则会进入到finally中执行,finally执行完毕后,会再把catch中的异常抛出。所以即使我们在try或catch中抛出了异常,也并不会影响finally的执行。

2.4 异常屏蔽

但是这时有的小伙伴又提问了,如果我们在执行finally语句中也抛出一个异常,又会怎么样呢?所以壹哥继续给大家进行论证,我们对上面的案例进行适当改造,在finally中也抛出一个异常。

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("非法参数异常");
    }
}

我们还是直接来看看执行结果:

从零开始学Java之详解抛出和声明异常的代码实现

从上面的结果中我们可以看出,在finally抛出异常后,原来catch中抛出的异常不见了。这是因为默认情况下只能抛出一个异常,而之前那个没被抛出的异常称为“被屏蔽的异常(Suppressed Exception) ”。

2.5 获取全部异常信息

但是有些较真的小伙伴就说,如果我就想知道所有的异常信息,包括catch中被屏蔽的那些异常信息,这该怎么办?

我们可以定义一个Exception的origin变量来保存原始的异常信息,然后调用 Throwable对象中的addSuppressed()方法 ,把原始的异常信息添加进来,最后在 finally中继续 抛出,并 利用throws关键字抛出Exception

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关键字抛出Exception
            throw e;
        }
    }
}

此时的执行结果如下所示:

从零开始学Java之详解抛出和声明异常的代码实现

从上面的结果中我们可以看出,即使catch和finally都抛出了异常时,catch异常被屏蔽,但我们通过addSuppressed方法,最终仍然获取到了完整的异常信息。但是我们也要知道,绝大多数情况下,都不应该在finally中抛出异常。

3. throws声明异常

对于方法中不想处理的异常,除了可以利用throw关键字进行抛出之外,还有别的办法吗?其实我们还可以在该方法的头部,利用throws关键字来声明这个不想处理的异常,把该异常传递到方法的外部进行处理。

3.1 基本语法

throws关键字的基本语法如下:

返回值类型 methodName(参数列表) throws Exception 1,Exception2,…{…}

通过这个语法,我们可以看出,在一个方法中可以利用throws关键字同时声明抛出多个异常:Exception 1,Exception2,… 多个异常之间利用","逗号分隔。如果被调用方法抛出的异常类型在这个异常列表中,则必须在该方法中捕获或继续向上层调用者抛出异常。而这里继续声明抛出的异常,可以是方法本身产生的异常,也可以是调用的其他方法抛出的异常。

3.2 代码实现

为了让大家理解throws关键字的用法,接下来壹哥继续设计一个案例进行说明。

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关键字声明抛出异常的执行逻辑是:

  1. 如果当前方法不知道如何处理某些异常,该异常可以交由更上一级的调用者来处理,比如main()方法;
  2. 如果main()方法不知道该如何处理该异常,也可以使用throws关键字继续声明抛出,该异常将交给JVM去处理;
  3. 最终JVM会打印出异常的跟踪栈信息,并中止程序运行,这也是程序在遇到异常后自动结束的原因。

3.4 注意事项

我们在使用throws时也要注意如下这些事项:

  • 只能在方法的定义签名处声明可能抛出的异常类型,否则编译器会报错;
  • 如果一个方法声明了抛出异常,但却没有在上层的方法体中对抛出的异常进行处理或继续抛出该异常,编译器会报错;
  • throws关键字只是声明方法可能抛出的异常类型,它并不一定真的会抛出异常;
  • 如果一个方法中可能会有多个异常抛出,可以使用逗号将它们分隔,如throws Exception1, Exception2, Exception3等;
  • 子类方法拋出的异常范围不能超过父类定义的范围。

4. throw与throws的区别

由于throw和throws长得特别像,功能也有类似之处,为了不让大家产生迷惑,所以壹哥给大家总结一下throw和throws的区别:

  • throw关键字用来抛出一个特定的异常对象,可以使用throw关键字手动抛出异常,执行throw一定会抛出某种异常对象;
  • throws关键字用于声明 一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常
  • throw需要用户自己捕获相关的异常,再对其进行相关包装,最后将包装后的异常信息抛出;
  • throws通常不必显示地捕获异常,可以由系统自动将所有捕获的异常信息抛给上层方法;
  • 我们通常在方法或类定义时,通过throws关键字声明该方法或类可能拋出的异常信息,而在方法或类的内部通过throw关键字声明一个具体的异常信息。

------------------------------正片已结束,来根事后烟----------------------------

二. 结语

至此,壹哥就把今天的内容讲解完毕了,但是异常的学习还没结束哦。下一篇文章中,壹哥会继续带大家学习异常中的新特性,敬请期待哦。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。