程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • JavaSE - 基础篇

    • Java环境搭建
    • Java基础语法
    • Java数据类型
    • Java常量和变量
    • Java进制和存储
    • Java运算符
    • Java流程控制
    • Java数组
    • Java面向对象上
    • Java面向对象下
    • Java异常机制
      • 前言
      • 异常体系结构
      • 异常之间的区别与联系
        • 1、Error
        • 2、运行时异常
        • 3、编译时异常
      • Java异常处理机制
        • 1、抛出异常
        • 2、捕获异常
        • 3、异常处理五个关键字
      • 处理异常
        • 1、try -catch
        • 2、thorw
        • 3、throws
        • 4、finally
        • 执行顺序
      • 自定义异常
        • 自定义异常规则
        • 自定义异常构建示例
        • 使用自定义异常
        • 注意事项
      • finally块和return
        • 1. finally块总是执行
        • 2. finally中的return会覆盖其他返回值
        • 3. finally中的return会抑制异常
        • 4. finally中抛出的异常会覆盖其他异常
      • 总结
    • Java枚举
    • Java反射机制
    • Java代理模式
    • Java泛型
    • Java序列化
    • Java多线程详解
    • Java线程池相关
  • Java
  • JavaSE - 基础篇
scholar
2023-11-22
目录

Java异常机制

# 前言

我们都知道在日常开发中,尽管努力地将代码写的尽善尽美,但在程序运行过程中,难免会出现一些奇奇怪怪的问题。有时通过代码很难去控制,比如:数据格式不对、网络不通畅、内存报警等。这就是Java的异常!

# 异常体系结构

Java把异常当作对象来处理,并定义一个基类 java.lang.Throwable作为所有异常的超类。

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。

Java异常层次结构图:

从图中可以看出所有异常类型都是内置类 Throwable 的子类,因而 Throwable 在异常类的层次结构的顶层。

接下来 Throwable 分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。

其中异常类 Exception 又分为运行时异常( RuntimeException )和编译时异常(Exception的其他子类)。

  • 非检查异常(Unchecked Exception),又称为运行时异常(RuntimeException),包括RuntimeException及其子类。这类异常发生的原因大多是程序逻辑错误,Java编译器不要求显式地处理这些异常(即,你的代码中可以不用try-catch捕获它们,也不需要通过throws声明它们可能被抛出)。虽然编译器不强制要求处理这些异常,但良好的程序设计应该考虑并处理可能发生的运行时异常,以提高程序的健壮性和稳定性。
  • 检查异常(Checked Exception),又称为编译时异常,是Exception的子类(但不包括RuntimeException及其子类)。这类异常发生的原因多样,可能是因为外部错误,如文件不存在、网络连接失败等。与非检查异常不同,Java编译器要求程序必须显式地处理这些异常。如果方法可能抛出某种检查异常,那么这个方法必须要么捕获这个异常,要么通过throws声明这个异常,否则程序无法通过编译。

# 异常之间的区别与联系

# 1、Error

在Java中,Error代表着严重的问题,这类问题通常是Java虚拟机(JVM)或硬件相关的问题,而非程序员可以通过代码逻辑来控制或处理的。Error及其子类对象表示的是编译时和系统错误,这些错误在通常情况下不被期望被程序捕获。

Error的典型例子包括:

  • OutOfMemoryError:当JVM没有足够的内存继续执行应用时抛出。
  • StackOverflowError:当应用递归调用过深,耗尽了调用栈的大小限制时抛出。
  • NoClassDefFoundError:当JVM因为某个类而不存在而失败时抛出。
  • LinkageError:当一个类依赖于某个不兼容或不存在的类时抛出。

由于Error表示基础运行时系统的异常情况,因此通常情况下,应用不应该尝试捕获这类错误。捕获这些错误通常不会提供恢复程序运行的途径,而且可能会隐藏真正的问题,使得问题难以调试。相反,遇到这种情况时,最好的处理方式通常是尽可能地收集错误发生时的环境信息,然后让程序终止。这样可以帮助开发者了解出现问题的根本原因,并且采取措施防止未来再次发生。

# 2、运行时异常

运行时异常是那些在执行期间可能被抛出的异常,它们都是RuntimeException的子类。这些异常是非受检异常,编译器不要求强制处理这些异常(即,你不需要用try-catch捕获它们,也不需要声明它们会被抛出)。它们通常由程序的错误导致,程序应该尽量避免这些错误。典型的运行时异常包括:

  • NullPointerException(空指针异常)
  • ClassCastException(类型转换异常)
  • ArithmeticException(算术异常,例如除数为0)
  • IllegalArgumentException(非法参数异常)
  • NumberFormatException(数字格式异常)
  • IllegalStateException(非法状态异常)
  • IndexOutOfBoundsException(下标越界异常),包括ArrayIndexOutOfBoundsException(数组下标越界异常)和StringIndexOutOfBoundsException(字符串下标越界异常)
  • NoSuchElementException(没有这样的元素异常)
  • InputMismatchException(输入不匹配异常)

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而 RuntimeException 之外的异常我们统称为非运行时异常,类型上属于 Exception 类及其子类。

注意: Error 和 Exception 的区别: Error 通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程; Exception 通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。

# 3、编译时异常

编译时异常是那些在编译期间必须被处理的异常,它们都是Exception的子类,但不包括RuntimeException及其子类。这些异常是受检异常,要求调用者必须处理这些异常,要么通过try-catch捕获它们,要么通过在方法签名中使用throws声明它们可能被抛出。典型的编译时异常包括:

  • IOException(输入输出异常),包括FileNotFoundException(文件未找到异常)、EOFException(文件结束异常)、MalformedURLException(URL格式异常)、UnknownHostException(未知主机异常)
  • SQLException(SQL数据库访问异常)
  • CloneNotSupportedException(对象克隆不支持异常)
  • ReflectiveOperationException(反射操作异常),包括ClassNotFoundException(类未找到异常)

# Java异常处理机制

java异常处理本质:抛出异常和捕获异常

# 1、抛出异常

要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息, 总能处理这个错误。

对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。

首先,是像创建普通的java对象一样将使用 new 在堆上创建一个异常对象;然后,当前的执行路径 (已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序

这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要 么换一种方式运行,要么继续运行下去。

举例:

假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。

if(stu == null){
    throw new NullPointerException();
}
1
2
3

# 2、捕获异常

在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

注意:

对于 运行时异常 、错误 和 检查异常 ,Java技术所要求的异常处理方式有所不同。

由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。

对于方法运行中可能出现的Error ,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数 Error 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。

# 3、异常处理五个关键字

分别是: try 、 catch 、 finally 、 throw 、 throws

try :用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

catch : 用于捕获异常。catch用来捕获try语句块中发生的异常。

finally :finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络 连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语 句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

throw : 用于抛出异常。

throws : 用在方法签名中,用于声明该方法可能抛出的异常。

# 处理异常

# 1、try -catch

try{
	//code that might generate exceptions
}catch(Exception e){
	//the code of handling exception1
}catch(Exception e){
	//the code of handling exception2
}
1
2
3
4
5
6
7

要明白异常捕获,还要理解 监控区域 (guarded region)的概念。它是一段可能产生异常的代码, 并且后面跟着处理这些异常的代码。

因而可知,上述 try-catch 所描述的即是监控区域,关键词 try后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java方法在运行过程中发生了异常,则创建异常对象。

将异常抛出监控区域之外,由Java运行时系统负责寻找匹配的 catch 子句来捕获异常。若有一个 catch 语句匹配到了,则执行该 catch 块中的异常处理代码,就不再尝试匹配别的 catch 块 了。

匹配原则:如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。

【演示】

public class TestException {
    public static void main(String[] args) {
        int a = 1;
        int b = 0;
        try { // try监控区域
            if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出
            异常
            System.out.println("a/b的值是:" + a / b);
            System.out.println("this will not be printed!");
        }
        catch (ArithmeticException e) { // catch捕捉异常
            System.out.println("程序出现异常,变量b不能为0!");
        }
        System.out.println("程序正常结束。");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

输出:

程序出现异常,变量b不能为0! 程序正常结束。

注意:显示一个异常的描述, Throwable 重载了 toString( ) 方法(由 Object 定义),所以 它将返回一个包含异常描述的字符串。例如,将前面的 catch 块重写成:

catch (ArithmeticException e) { // catch捕捉异常
    System.out.println("程序出现异常" + e);
}
//输出
// 程序出现异常java.lang.ArithmeticException
1
2
3
4
5

算术异常属于运行时异常,因而实际上该异常不需要程序抛出,运行时系统自动抛出。如果不用try-catch程序就不会往下执行了。

【演示】

public class TestException {
    public static void main(String[] args) {
        int a = 1;
        int b = 0;
        System.out.println("a/b的值是:" + a / b);
        System.out.println("this will not be printed!");
    }
}
/*
结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at TestException.main(TestException.java:7)
*/
1
2
3
4
5
6
7
8
9
10
11
12
13

使用多重的catch语句:很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的 catch 子句,每个子句捕获一种类型的异常,当异常被引发时,每个 catch 子 句被依次检查,第一个匹配异常类型的子句执行,当一个 catch 子句执行以后,其他的子句将被旁路。

编写多重catch语句块注意事项:

顺序问题:先小后大,即先子类后父类

注意:

Java通过异常类描述异常类型。对于有多个 catch 子句的异常程序而言,应该尽量将捕获底层异常类的 catch 子句放在前面,同时尽量将捕获相对高层的异常类的 catch 子句放在后面。否则,捕获 底层异常类的 catch 子句将可能会被屏蔽。

嵌套try语句: try 语句可以被嵌套。也就是说,一个 try 语句可以在另一个 try 块的内部。每次进入 try 语句,异常的前后关系都会被推入堆栈。如果一个内部的 try 语句不含特殊异常的 catch 处理程序,堆栈将弹出,下一个 try 语句的 catch 处理程序将检查是否与之匹配。这个 过程将继续直到一个 catch 语句被匹配成功,或者是直到所有的嵌套 try 语句被检查完毕。如果没有 catch 语句匹配,Java运行时系统将处理这个异常。

【演示】

public class NestTry {
    public static void main(String[] args) {
        try {
            int a = args.length;
            int b = 42 / a;
            System.out.println("a = " + a);
            try {
                if (a == 1) {
                    a = a / (a - a);
                }
                if (a == 2) {
                    int c[] = {1};
                    c[42] = 99;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("ArrayIndexOutOfBounds :" + e);
            }
        } catch (ArithmeticException e) {
            System.out.println("Divide by 0" + e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

分析运行:

javac NestTry.java

java NestTry one a = 1 Divide by 0java.lang.ArithmeticException: / by zero

java NestTry one two a = 2 ArrayIndexOutOfBounds :java.lang.ArrayIndexOutOfBoundsException: 42

分析:正如程序中所显示的,该程序在一个try块中嵌套了另一个 try 块。程序工作如下:当你在没有命令行参数的情况下执行该程序,外面的 try 块将产生一个被0除的异常。

程序在有一个命令行参数条件下执行,由嵌套的 try 块产生一个被0除的异常,由于内部的 catch 块不匹配这个异常,它将把异常传给外部的 try 块,在外部异常被处理。如果你在具有两个命令行参数的条件下执行该程序,将由内部 try 块产生一个数组边界异常。

注意:当有方法调用时, try 语句的嵌套可以很隐蔽的发生。例如,我们可以将对方法的调用放在一 个 try 块中。在该方法的内部,有另一个 try 语句。

在这种情况下,方法内部的 try 仍然是嵌套在外部调用该方法的 try 块中的。下面我们将对上述例子进行修改,嵌套的 try 块移到方法nesttry()的内部:结果依旧相同!

public class NestTry {
    static void nesttry(int a) {
        try {
            if (a == 1) {
                a = a / (a - a);
            }
            if (a == 2) {
                int c[] = {1};
                c[42] = 99;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBounds :" + e);
        }
    }

    public static void main(String[] args) {
        try {
            int a = args.length;
            int b = 42 / a;
            System.out.println("a = " + a);
            nesttry(a);
        } catch (ArithmeticException e) {
            System.out.println("Divide by 0" + e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2、thorw

到目前为止,我们只是获取了被Java运行时系统引发的异常。然而,我们还可以用throw语句抛出明确的异常。

语法形式:throw ThrowableInstance;

这里的ThrowableInstance一定是 Throwable 类类型或者 Throwable 子类类型的一个对象。简单的数据类型,例如 int ,char 以及非 Throwable 类,例如 String 或 Object ,不能用作异常。

有两种方法可以获取 Throwable 对象:在 catch 子句中使用参数或者使用 new 操作符创建。程序执行完 throw 语句之后立即停止;throw 后面的任何语句不被执行,最邻近的 try 块用来检 查它是否含有一个与异常类型匹配的 catch 语句。

如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的 try 块来检查,以此类推。如果没有发现匹配的 catch 块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。

class TestThrow {
    static void proc() {
        try {
            throw new NullPointerException("demo");
        } catch (NullPointerException e) {
            System.out.println("Caught inside proc");
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            proc();
        } catch (NullPointerException e) {
            System.out.println("Recaught: " + e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

该程序两次处理相同的错误,首先, main() 方法设立了一个异常关系然后调用proc( )。proc( )方法设立了另一个异常处理关系并且立即抛出一个 NullPointerException 实例,NullPointerException 在 main() 中被再次捕获。

该程序阐述了怎样创建Java的标准异常对象,特别注意这一行:

throw new NullPointerException("demo");
1

分析:此处 new 用来构造一个 NullPointerException 实例,所有的Java内置的运行时异常有两个构造方法:一个没有参数,一个带有一个字符串参数。

当用第二种形式时,参数指定描述异常的字符串。如果对象用作 print( ) 或者 println( ) 的参数 时,该字符串被显示。这同样可以通过调用getMessage( )来实现,getMessage( )是由 Throwable 定义的。

# 3、throws

如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。要做到这点,我们可以在方法声明中包含一个 throws 子句。

一个 throws 子句列举了一个方法可能引发的所有异常类型。这对于除了 Error 或 RuntimeException 及它们子类以外类型的所有异常是必要的。一个方法可以引发的所有其他类型的异常必须在 throws 子句中声明,否则会导致编译错误。

public void info() throws Exception
{
    //body of method
}
1
2
3
4

Exception 是该方法可能引发的所有的异常,也可以是异常列表,中间以逗号隔开。

【例子】

class TestThrows{
    static void throw1(){
        System.out.println("Inside throw1 . ");
        throw new IllegalAccessException("demo");
    }
    
    public static void main(String[] args){
        throw1();
    }
}
1
2
3
4
5
6
7
8
9
10

上述例子中有两个地方存在错误,你看出来了吗?

该例子中存在两个错误,首先,throw1( )方法不想处理所导致的异常,因而它必须声明 throws 子句 来列举可能引发的异常即 IllegalAccessException ;其次, main() 方法必须定义 try/catch 语句来捕获该异常。

正确例子如下:

class TestThrows {
    static void throw1() throws IllegalAccessException {
        System.out.println("Inside throw1 . ");
        throw new IllegalAccessException("demo");
    }

    public static void main(String[] args) {
        try {
            throw1();
        } catch (IllegalAccessException e) {
            System.out.println("Caught " + e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

throws 抛出异常的规则:

  • 如果是不受检查异常( unchecked exception ),即 Error 、 RuntimeException 或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
  • 必须声明方法可抛出的任何检查异常( checked exception )。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误
  • 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
  • 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。

# 4、finally

当异常发生时,通常方法的执行将做一个陡峭的非线性的转向,它甚至会过早的导致方法返回。例如, 如果一个方法打开了一个文件并关闭,然后退出,你不希望关闭文件的代码被异常处理机制旁路。 finally 关键字为处理这种意外而设计。

finally 创建的代码块在 try/catch 块完成之后另一个 try/catch 出现之前执行。 finally 块无论有没有异常抛出都会执行。如果抛出异常,即使没有 catch 子句匹配, finally 也会执行。

一个方法将从一个 try/catch 块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句, finally 子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用。

注意: finally 子句是可选项,可以有也可以无,但是每个 try 语句至少需要一个 catch 或 者 finally 子句。

【例子】

class TestFinally {
    static void proc1() {
        try {
            System.out.println("inside proc1");
            throw new RuntimeException("demo");
        } finally {
            System.out.println("proc1's finally");
        }
    }

    static void proc2() {
        try {
            System.out.println("inside proc2");
            return;
        } finally {
            System.out.println("proc2's finally");
        }
    }

    static void proc3() {
        try {
            System.out.println("inside proc3");
        } finally {
            System.out.println("proc3's finally");
        }
    }

    public static void main(String[] args) {
        try {
            proc1();
        } catch (Exception e) {
            System.out.println("Exception caught");
        }
        proc2();
        proc3();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

执行结果:

inside proc1 proc1's finally Exception caught inside proc2 proc2's finally inside proc3 proc3's finally

注:如果 finally 块与一个 try 联合使用, finally 块将在 try 结束之前执行。

# 执行顺序

try, catch,finally ,return 执行顺序

  1. 执行try,catch , 给返回值赋值
  2. 执行finally
  3. return

# 自定义异常

自定义异常是Java异常处理机制的一部分,允许你根据自己的需求创建专门的异常。自定义异常可以让错误处理更加细致和清晰,有助于调试和维护代码。以下是自定义异常的关键步骤和规则:

# 自定义异常规则

  • 扩展异常类:自定义异常通过扩展Exception类(检查异常)或RuntimeException(非检查异常)来创建。
  • 构造方法:建议提供四种类型的构造方法,以便在不同情况下创建异常实例:
    • 无参构造函数:public MyException() { super(); }
    • 带有字符串参数的构造函数:public MyException(String message) { super(message); }
    • 带有字符串和原因(Throwable)参数的构造函数:public MyException(String message, Throwable cause) { super(message, cause); }
    • 带有Throwable参数的构造函数:public MyException(Throwable cause) { super(cause); }

# 自定义异常构建示例

public class MyException extends Exception {
	public MyException() {
		super();
	}

	public MyException(String message) {
		super(message);
	}

	public MyException(String message, Throwable cause) {
		super(message, cause);
	}

	public MyException(Throwable cause) {
		super(cause);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 使用自定义异常

在使用自定义异常时,应当在适当的地方通过throw关键字抛出异常实例,并在调用方法的地方用try...catch捕获并处理这些异常。这种方式可以将异常的创建和处理分离,增加代码的可读性和维护性。

public class Test {
    public static void main(String[] args) {
        A a = new A();
        try {
            a.show(-2);
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}

class A {
    public void show(int num) throws MyException {
        if (num < 0) {
            throw new MyException("异常:" + num + "不是正数");
        }
        System.out.println(num);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

image-20240317175434060

# 注意事项

  • 避免在finally块中使用return或抛出异常,这可能会覆盖或抑制try块或catch块中的返回值和异常。
  • 尽量让自定义异常的使用清晰明确,不要滥用自定义异常,确保它们能为错误处理和代码维护带来实际的好处。

自定义异常提供了一种灵活的方式来处理特定的错误情况,它们是Java异常处理机制的重要组成部分。

# finally块和return

# 1. finally块总是执行

即使try块中有return语句,finally块仍然会执行。这保证了即便是在方法返回前,资源清理等重要操作都能得到执行。

public static void main(String[] args){
    int re = bar();
    System.out.println(re);
}
private static int bar() {
    try{
        return 5;
    } finally{
        System.out.println("finally");
    }
}
1
2
3
4
5
6
7
8
9
10
11

image-20240317175526264

# 2. finally中的return会覆盖其他返回值

如果finally块中包含return语句,它将会覆盖try块或catch块中的返回值。这是因为finally块总是最后执行的,所以其return语句会是方法的最终返回语句。

public static void main(String[] args){
    int result;

    result  =  foo();
    System.out.println(result);     //2

    result = bar();
    System.out.println(result);    //2
}

@SuppressWarnings("finally")
public static int foo(){
    try{
        int a = 5 / 0;
    } catch (Exception e){
        return 1;
    } finally{
        return 2;
    }

}

@SuppressWarnings("finally")
public static int bar(){
    try {
        return 1;
    }finally {
        return 2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

image-20240317175733748

# 3. finally中的return会抑制异常

如果try或catch块中抛出一个异常,但finally块中有return语句,则原来的异常会被忽略,因为方法会从finally块中退出,异常不会被抛出。

class TestException {
    public static void main(String[] args) {
        int result;
        try {
            result = foo();
            System.out.println(result);           //输出100
        } catch (Exception e) {
            System.out.println(e.getMessage());    //没有捕获到异常
        }
 
 
        try {
            result = bar();
            System.out.println(result);           //输出100
        } catch (Exception e) {
            System.out.println(e.getMessage());    //没有捕获到异常
        }
    }
 
    // catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception {
        try {
            int a = 5 / 0;
            return 1;
        } catch (ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中使用了return");
        } finally {
            return 100;
        }
    }
 
    // try中的异常被抑制
    // J2SE 提供的最后一个批注是 @SuppressWarnings。
    // 该批注的作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默。
    @SuppressWarnings("finally")
    public static int bar() throws Exception {
        try {
            int a = 5 / 0;
            return 1;
        } finally {
            return 100;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

image-20240317175814233

# 4. finally中抛出的异常会覆盖其他异常

如果finally块抛出一个异常,它会覆盖try块或catch块中的任何其他异常。这是因为finally块总是最后执行,其抛出的异常会是方法抛出的最终异常。

public class TestException {
    public static void main(String[] args) {
        int result;
        try {
            result = foo();
        } catch (Exception e) {
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
 
 
        try {
            result = bar();
        } catch (Exception e) {
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
    }
 
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception {
        try {
            int a = 5 / 0;
            return 1;
        } catch (ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
        } finally {
            throw new Exception("我是finally中的Exception");
        }
    }
 
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception {
        try {
            int a = 5 / 0; // 这里会抛出算术异常
            return 1;
        } finally {
            throw new Exception("我是finally中的Exception");
        }
 
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

image-20240317175927434

上面的3个例子都异于常人的编码思维,因此我建议:

  • 尽量不要在finally块中使用return语句,以避免覆盖try块或catch块的返回值。
  • 尽量不要在finally块中抛出异常,以避免覆盖try块或catch块中的异常。
  • 让finally块尽量只做资源释放等清理工作,避免执行其他可能影响程序逻辑的操作。
  • 将返回语句放在方法的末尾,而不是在try...catch...finally结构中,以提高代码的可读性和可维护性。

# 总结

实际应用中的经验与总结

  1. 处理运行时异常时,采用逻辑去合理规避同时辅助try-catch处理

  2. 在多重catch块后面,可以加一个catch ( Exception )来处理可能会被遗漏的异常

  3. 对于不确定的代码,也可以加上try-catch,处理潜在的异常

  4. 尽量去处理异常,切忌只是简单的调用printStackTrace)去打印输出

  5. 具体如何处理异常,要根据不同的业务需求和异常类型去决定

  6. 尽量添加finally语句块去释放占用的资源

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Java面向对象下
Java枚举

← Java面向对象下 Java枚举→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式