likes
comments
collection
share

从零开始学Java之异常到底该如何捕获和处理?

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

前言

在上一篇文章中,壹哥带大家简单认识了Java中的异常处理机制,但这些内容主要是偏重于理论,接下来我会带大家学习具体的代码实现。现在我们知道,Java的异常处理是通过5个关键字来实现的,即try、catch、throw、throws和finally。try catch语句用于捕获并处理异常,finally语句用于在任何情况下(除特殊情况外,比如提前调用了System.exit()退出虚拟机的方法)都必须执行的代码,throw语句用于拋出异常,throws语句用于声明可能会出现的异常。

虽然如此,但具体该怎么捕获异常?怎么抛出异常?什么时候抛?什么时候捕?这些对初学者来说都是需要认真掌握的。

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

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

配套开源项目资料

Github: github.com/SunLtd/Lear…

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

一. 捕获和处理异常

1. 概述

在Java中,如果某行或某几行代码有可能会抛出异常,我们此时就可以用try ... catch ... finally进行捕获处理。把可能发生异常的语句放在try { ... }语句中,然后使用catch语句捕获对应的Exception及其子类,把必须执行的代码放在finally语句中。接下来我们就来看看具体的代码实现吧。

2. try-catch结构

2.1 基本语法

首先我们来看看try-catch的基本语法:

try {
    // 可能发生异常的语句
} catch(Exception e) {
    // 处理异常语句
}

在上面的语法中,我们要把可能引发异常的语句封装在try语句块中,用于捕获可能发生的异常。catch语句里的( )中,要传入与try语句里匹配的异常类,指明catch语句可以处理的异常类型。

也就是说,如果此时try语句块中发生了某个异常,那么这个相应的异常对象就会被拋出,产生异常的这行代码之后的其余代码就不会再被执行。然后catch语句就开始执行,把try中拋出的异常对象进行捕获并处理。 当然,如果try语句块中没有发生异常,那么try里的代码就会正常执行结束,而后面的catch就会被跳过。

这里我们需要注意:try...catch与if...else是不一样的,try后面的花括号{ }不可以省略,即便try中只有一行代码。同样的,catch的花括号 { } 也不可以省略。另外,try语句块中声明的变量属于局部变量,它只在try中有效,其它地方不能访问该变量。

2.2 代码实现

接下来壹哥设计一个代码案例,来讲解try-catch的用法:

/**
 * @author 一一哥Sun
 */
public class Demo01 {
    public static void main(String[] args) {
        //定义一个长度为3的数组
        int[] array = new int[3];
        try {
            //索引超出了数组长度,将会引发ArrayIndexOutOfBoundsException数组下标越界异常
            array[3] = 1; 
            //发生异常后,这行代码并不会执行
            System.out.println("数组:" + array.toString());
        } catch (ArrayIndexOutOfBoundsException e) {
            //指出异常的类型、性质、栈层次及出现在程序中的位置
            e.printStackTrace();
            //输出错误的原因及性质
            System.out.println("数组越界:" + e.getMessage());
            //输出异常的类型与性质
            System.out.println("数组越界:" + e.toString());
        }
    }
}

在上面这段代码中,壹哥在try语句中创建了一个长度为3的整数数组,并尝试着将第4个位置上的元素值设为1。由于数组越界,这会引发代码故障,java会抛出一个ArrayIndexOutOfBoundsException异常。由于发生了异常,所以后面的数组输出语句就不会被执行。

而我们在catch中接收了ArrayIndexOutOfBoundsException异常,这个异常与try中抛出的异常是一致的,所以代码会跳转到catch中进行异常的处理。另外在上面的处理代码块中,我们可以通过Exception异常对象的以下方法,来获取到相应的异常信息。

  • printStackTrace()方法:输出异常栈信息,指出异常的类型、性质、栈层次及出现在程序中的位置;
  • getMessage()方法:输出错误的性质;
  • toString()方法:输出异常的类型与性质。

2.3 配套视频

本节内容配套视频链接如下:

player.bilibili.com/player.html…

3. 多重catch结构

我们在编写java代码时,多行语句有可能会产生多个不同的异常,你们面对这个多个异常该怎么处理呢?其实我们可以使用多重catch语句来分别处理多个异常。

3.1 基本语法

多重catch语句的基本语法格式如下:

try {
    // 可能会发生异常的语句
} catch(ExceptionType e) {
    // 处理异常语句
} catch(ExceptionType e) {
    // 处理异常语句
} catch(ExceptionType e) {
    // 处理异常语句
	...
}

当存在多个catch代码块时,只要有一个catch代码块捕获到一个异常,其它的catch代码块就不再进行匹配。但是我们要注意,当捕获的多个异常类之间存在父子关系时,一般是先捕获子类,再捕获父类。所以我们在编写代码时,要把子类异常放在父类异常的前面,否则子类就会捕获不到。

3.2 代码实现

接下来壹哥给大家设计了一个利用IO流读取文件内容的案例,这段代码就可能会出现两个异常。

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

/**
 * @author 一一哥Sun
 */
public class Demo02 {
    //多重catch语句
    public static void main(String[] args) {
        //定义一个缓冲流对象,以后在IO流阶段壹哥会细讲
        BufferedReader reader = null;
        try {
            //对接一个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();
            }
        } catch (FileNotFoundException e) {
        	//处理文件不存在时的异常
            System.out.println("文件不存在:" + e.getMessage());
        } catch (IOException e) {
        	//处理IO异常
            System.out.println("读取文件失败:" + e.getMessage());
        } 
	}
}

在这段代码中,我们尝试打开一个名为file.txt的文件,并逐行读取其内容。如果该文件不存在或读取失败,程序将会在相应的catch块中处理异常,并打印出异常消息。具体地来说,就是try代码块的第16行代码调用了FileReader的构造方法,这里有可能会发生FileNotFoundException异常。而第18行调用BufferedReader输入流的readLine()方法时,有可能会发生IOException异常。

因为FileNotFoundException异常是IOException异常的子类,所以我们应该先捕获 FileNotFoundException异常,即第23行代码;后捕获IOException异常,即第26行代码。但是如果我们将FileNotFoundException和IOException异常的捕获顺序进行调换,那么捕获FileNotFoundException异常的代码块永远也不会执行,所以FileNotFoundException异常也永远不会被处理。当然,如果多个异常之间没有父子关系,则其顺序就无所谓了。

4. try-catch-finally结构

在上面的代码中,我们涉及到了IO流的相关内容,这一块我们还没有开始进行学习,壹哥会在以后给大家进行详细地讲解。

IO流属于一种比较消耗物理资源的API,使用完之后应该把IO流进行关闭,否则就可能会导致物理资源被过多的消耗。那么我们该在哪里关闭IO流呢?有的人会说,我们可以在try语句块执行完正常的功能后关闭IO流。但是大家要知道,try语句块和catch语句块有可能因为异常并没有被完全执行,那么try里打开的这些物理资源到底要在哪里回收呢?

为了确保这些物理资源一定可以被回收,异常处理机制给我们提供了finally代码块,且Java 7之后又提供了自动资源管理(Automatic Resource Management)技术,更是可以优雅地解决资源回收的问题。

4.1 基本语法

无论是否发生异常,finally里的代码总会被执行。在 finally代码块中,我们可以执行一些清理等收尾善后性质的代码,其基本语法格式如下:

try {
    // 可能会发生异常的语句
} catch(ExceptionType e) {
    // 处理异常语句
} finally {
    // 执行清理代码块,这里的代码肯定会被执行到,除非极特殊的情况发生
}

上面的代码中,无论是否发生了异常(除极特殊的情况外,比如提前调用了System.exit()退出虚拟机的方法),finally语句块中的代码都会被执行。另外,finally语句也可以直接和try语句配合使用,其语法格式如下:

try {
    // 逻辑代码块
} finally {
    // 清理代码块
}

我们在使用try-catch-finally语句时要注意以下几点:

  1. 在异常处理的语法结构中,只有try是必需的。如果没有try代码块,则不能有后面的catch和finally;
  2. 虽然catch块和finally块是可选的,但也不能只有try块,catch块和finally块至少要出现其中之一,也可以同时出现;
  3. 可以有多个catch块,捕获父类异常的catch块,必须位于捕获子类异常的后面;
  4. 多个catch块必须位于try块之后,finally块必须位于所有的catch块之后;
  5. 只有finally与try语句块的语法格式,这种情况会导致异常的丢失,所以并不常见;
  6. 通常情况下,我们不应该在finally代码块中使用return或throw等会导致方法终止的语句,否则这将会导致try和catch代码块中的return和throw语句失效。

尤其是try-catch-finally与return的结合,是我们面试时的一个考点哦。有些面试官会贱贱地问你try-catch-finally中如果有return,会发生什么,请大家自行做个实验吧。

4.2 代码实现

接下来壹哥在之前的案例基础上进行改造,引入finally代码块:

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

/**
 * @author 一一哥Sun
 */
public class Demo03 {
    //try-catch-finally语句
    public static void main(String[] args) {
        //定义一个缓冲流对象,以后在IO流阶段壹哥会细讲
        BufferedReader reader = null;
        try {
        	//对接一个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();
            }
        } catch (FileNotFoundException e) {
        	//处理文件不存在时的异常
            System.out.println("文件不存在:" + e.getMessage());
        } catch (IOException e) {
            //处理IO异常
            System.out.println("读取文件失败:" + e.getMessage());
        } finally {
            try {
                //在finally代码块中也可以进行try-catch的操作
                if (reader != null) {
                	//关闭IO流
                    reader.close();
                }
            } catch (IOException e) {
                System.out.println("关闭文件失败:" + e.getMessage());
            }
        }
	}
}

在这段代码中,我们尝试打开一个名为file.txt的文件,并逐行读取其内容。如果文件不存在或读取失败,程序将在相应的catch块中处理异常,并打印出异常消息。无论是否发生异常,程序都会在finally块中关闭文件。

4.3 小结

在try-catch-finally组合的结构中,其执行流程如下图所示:

从零开始学Java之异常到底该如何捕获和处理?

根据该流程可知,try-catch-finally语句块的执行情况可以细分为以下几种情况:

  1. 如果try代码块中没有拋出异常,则执行完try代码块后会直接执行finally代码块;
  2. 如果try代码块中拋出了异常,并被catch子句捕捉,则终止try代码块的执行,转而执行相匹配的 catch代码块,之后再执行 finally代码块;
  3. 如果try代码块中拋出的异常没有被任何catch子句捕获到,将会直接执行finally代码块中的语句,并把该异常传递给该方法的调用者;
  4. 如果在finally代码块中也拋出了异常,则会把该异常传递给该方法的调用者。

4.4 配套视频

本节内容配套视频链接如下:

player.bilibili.com/player.html…

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

二. 结语

至此,壹哥就把今天的内容讲解完毕了,通过今天的内容,大家要注意以下几点:

  • try...catch与if...else是不一样的,try后面的花括号{ }不可以省略,即便try中只有一行代码;
  • 同样的,catch的花括号 { } 也不可以省略
  • 当捕获的多个异常类之间存在父子关系时,一般是先捕获子类,再捕获父类;
  • 在异常处理的语法结构中,只有try是必需的。如果没有try代码块,则不能有后面的catch和finally。

在下一篇文章中,壹哥会带大家学习如何声明和抛出异常,敬请期待哦。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。