Java异常机制:从混乱到控制的错误管理艺术

在这里插入图片描述

  • 👑专栏内容:Java
  • ⛪个人主页:子夜的星的主页
  • 💕座右铭:前路未远,步履不停

目录

  • 一、异常的体系结构
    • 1、异常的体系结构
    • 2、异常的分类
  • 二、异常的处理
    • 1、异常的抛出
    • 2、异常的捕获
      • 2.1、异常声明`throws`
      • 2.2、`try-catch`捕获并处理
      • 2.3、`finally`
    • 3、异常的处理流程
      • 3.1、什么是 "调用栈"
      • 3.2、异常处理流程总结
  • 三、自定义异常类


在程序运行过程中,会遇见一些奇奇怪怪的问题,有时候通过代码是很难控制的。在 Java 中,将程序执行过程中发生的不正常行为称之为异常。

一、异常的体系结构

异常种类有很多,为了对不同异常或者错误进行很好的分类管理。

Java内部维护了一个异常的体系结构:
在这里插入图片描述

1、异常的体系结构

Throwable 是 Java异常体系的顶层类,是Java语言中所有错误和异常的超类。它派生出两个重要的子类:ErrorException

Error 指的是Java虚拟机(JVM)无法解决的严重问题,通常不应由合理的应用程序尝试捕获。大多数此类错误是JVM遇到的异常情况。此类异常一旦发生,一般没有办法通过修改代码解决。

Exception指的是程序员可以通过代码进行处理,使程序继续执行的异常。我们平时所说的“异常”通常是指Exception

2、异常的分类

异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:运行时异常和编译时异常(受检查异常)。

  • 编译时异常

在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)。这些异常在编译期间会被检查,也就是说,如果一个方法可能抛出某个受检查异常,那么在调用这个方法的地方必须对这个异常进行处理。

这种处理可以是捕获异常并对其进行处理,或者是将异常声明在方法的throws子句中,告诉方法的调用者需要处理这个异常。

  • 运行时异常

运行时异常,也称为非受检查异常(Unchecked Exceptions),与编译时异常不同,运行时异常在编译期间不需要显式处理。这意味着如果你的代码抛出了一个运行时异常,你不需要用try-catch块捕获它,也不需要在方法声明中用throws子句声明它。

二、异常的处理

异常处理主要的5个关键字:throwtrycatchfinalthrows

1、异常的抛出

throw关键字是用来显式地抛出一个异常的。使用throw可以抛出一个现有的异常实例或创建一个新的异常实例并抛出。具体的语法如下:

throw new XXXException("需要显示的错误信息");

我们一般通过throw抛出一些自己自定义的异常。

public class ArrayElementRetriever {// getElement方法尝试从一个整数数组中获取指定索引处的元素public static int getElement(int[] array, int index) {// 检查传入的数组是否为null。如果是null,抛出NullPointerException。if (null == array) {throw new NullPointerException("传递的数组为null");}// 检查索引是否越界。如果索引小于0或大于等于数组长度,抛出ArrayIndexOutOfBoundsException。if (index < 0 || index >= array.length) {throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");}// 如果没有异常发生,则返回数组中指定索引处的元素。return array[index];}public static void main(String[] args) {int[] array = {1, 2, 3};// 尝试获取数组中的元素。这里故意传递一个越界的索引。getElement(array, 3);}
}

【注意事项】

  1. throw必须写在方法体内部。
  2. 抛出的对象必须是Exception 或者 Exception 的子类对象。
  3. 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理(JVM会立即终止你的程序)。
  4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。
  5. 异常一旦抛出,其后的代码就不会执行。

2、异常的捕获

异常的捕获,也就是异常的具体处理方式。异常的捕获主要有两种方式:异常声明throws 以及 try-catch捕获处理。

2.1、异常声明throws

处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。具体语法如下:

修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
public class ExceptionThrower {// 定义一个方法,声明它可能抛出一个自定义的编译时异常public void doSomethingRisky() throws CustomException {// 假设这里有一些逻辑,最后确定需要抛出异常throw new CustomException("描述错误情况");}public static void main(String[] args) {ExceptionThrower thrower = new ExceptionThrower();try {// 尝试调用可能抛出异常的方法thrower.doSomethingRisky();} catch (CustomException e) {// 处理异常System.out.println("捕获到异常: " + e.getMessage());}}
}// 自定义编译时异常
class CustomException extends Exception {public CustomException(String message) {super(message);}
}

【注意事项】

  1. throws关键字用于方法声明中,紧跟在方法的括号之后。
  2. throws后面可以跟一个或多个异常类型,使用逗号分隔。
  3. 如果一个方法声明抛出一个异常,那么调用这个方法的代码必须处理这个异常,要么是通过try-catch捕获它,要么是通过在其自身声明中使用throws继续向上抛出。
  4. 使用throws声明的异常类型应该是具体的异常类,而不是抽象类或接口。
  5. 对于运行时异常(RuntimeException及其子类),通常不在方法声明中使用throws进行声明,因为它们是非受检查的异常,但你也可以这样做以提供更好的程序文档。

2.2、try-catch捕获并处理

throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。具体语法格式如下:

try{// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}[catch(异常类型 e){// 对异常进行处理
}finally{// 此处代码一定会被执行到
}]// 后序代码// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行

注意:

  1. []中表示可选项,可以添加,也可以不用添加
  2. try中的代码可能会抛出异常,也可能不会
public class demo{public static void main(String[] args) {System.out.println("before");try{System.out.println(10/0);}catch (ArithmeticException e){System.out.println("捕获到了 ArithmeticException 这个异常");}System.out.println("affer");}
}

在这里插入图片描述 try块内抛出异常位置之后的代码将不会被执行,如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序。

public class demo1 {public static void main(String[] args) {System.out.println("before");try{System.out.println(10/0);}catch (NullPointerException e){System.out.println("捕获到了 ArithmeticException 这个异常");}System.out.println("affer");}
}

在这里插入图片描述try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获,即多种异常,多次捕获

public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");// arr = null;System.out.println(arr[100]);System.out.println("after");} catch (ArrayIndexOutOfBoundsException e) {System.out.println("这是个数组下标越界异常");e.printStackTrace();} catch (NullPointerException e) {System.out.println("这是个空指针异常");e.printStackTrace();}System.out.println("after try catch");}
}

在这里插入图片描述如果多个异常的处理方式是完全相同, 也可用写成下面这个样子:

catch (ArrayIndexOutOfBoundsException | NullPointerException e) {}

虽然可用像上面这样写,但是不建议。因为没办法知道到底是因为那个异常导致的。衡量一个代码的好与坏,除了时间复杂度和空间复杂度之外,真正要看的是代码的可读性。

如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误:

public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");arr = null;System.out.println(arr[100]);System.out.println("after");} catch (Exception e) { // Exception可以捕获到所有异常e.printStackTrace();}catch (NullPointerException e){ // 永远都捕获执行到e.printStackTrace();}System.out.println("after try catch");}
}

在这里插入图片描述 可以通过一个catch捕获所有的异常,即多个异常,一次捕获(不推荐)

public class demo1 {public static void main(String[] args) {int[] arr = {1, 2, 3};try {System.out.println("before");arr = null;System.out.println(arr[100]);System.out.println("after");} catch (Exception e) {e.printStackTrace();}System.out.println("after try catch");}
}

在这里插入图片描述由于 Exception 类是所有异常类的父类,因此可以用这个类型表示捕捉所有异常。

catch 进行类型匹配的时候,不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象。如刚才的代码,NullPointerExceptionArrayIndexOutOfBoundsException 都是 Exception 的子类,因此都能被捕获到。

2.3、finally

在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。

另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。finally的语法格式如下:

try{// 可能会发生异常的代码
}catch(异常类型 e){// 对捕获到的异常进行处理
}finally{// 此处的语句无论是否发生异常,都会被执行到
}// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public class demo1 {public static void main(String[] args) {try{int[] arr = {1,2,3};arr[100] = 10;arr[0] = 10;}catch (ArrayIndexOutOfBoundsException e){System.out.println(e);}finally {System.out.println("finally中的代码一定会执行");}System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");}
}

在这里插入图片描述
【问题】 既然 finallytry-catch-finally 后的代码都会执行,那为什么还要有finally呢?

虽然finally 块和try-catch 结构后的代码在很多情况下都会执行,finally 仍然非常重要,原因主要包括:

  1. 确保资源释放: 最常见的finally用途是进行资源清理,比如关闭文件流或数据库连接。这些操作即使在发生异常时也必须执行,以防止资源泄漏。如果只将清理代码放在try-catch之后,那么在异常未被当前的catch捕获时,这些清理代码将不会执行。

  2. 处理未捕获的异常: 如果try块中抛出了一个未被任何catch捕获的异常,try-catch之后的代码不会执行,但finally块仍然会执行。这为处理所有情况提供了一个统一的地方,确保即使在出现意外异常时也能进行必要的清理。

  3. 覆盖返回值:trycatch块中有返回语句时,finally块仍会在方法返回之前执行,甚至有能力修改返回值(但是并不推荐这样,因为会使代码难以理解)。

  4. 明确的意图: 使用finally可以明确表示某段代码无论发生何种情况都必须执行,这对于代码的可读性和维护性是有好处的。它让其他开发者清楚地知道,无论try块中发生了什么,finally块中的代码都是必须执行的。

3、异常的处理流程

3.1、什么是 “调用栈”

方法之间是存在相互调用关系的,这种调用关系我们可以用 “调用栈” 来描述。 在 JVM 中有一块内存空间称为"虚拟机栈" 专门存储方法之间的调用关系。当代码中出现异常的时候,我们就可以使用 e.printStackTrace(); 的方式查看出现异常代码的调用栈。这个调用栈追踪显示了异常发生时的方法调用顺序,从最近的方法调用开始,一直追溯到异常被抛出的源头。调用栈追踪提供的信息通常包括:

  1. 异常类型和描述信息。
  2. 异常发生的代码位置,包括类名、文件名和行号。
  3. 方法调用序列,从发生异常的方法开始,一直到程序入口。
public class demo1 {public static void main(String[] args) {try {func();} catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace();}System.out.println("after try catch");}public static void func() {int[] arr = {1, 2, 3};System.out.println(arr[100]);}
}

在这里插入图片描述
如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理,程序就会异常终止(和我们最开始未使用 try catch 时是一样的)。

3.2、异常处理流程总结

  1. 程序先执行 try 中的代码。
  2. 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配。
  3. 如果找到匹配的异常类型, 就会执行 catch 中的代码。
  4. 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
  5. 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)。
  6. 如果上层调用者也没有处理的了异常, 就继续向上传递一直到 main 方法也没有合适的代码处理异常, 。就会交给 JVM 来进行处理, 此时程序就会异常终止。

三、自定义异常类

自定义异常是一种用户定义的异常,它可以帮助处理程序特定的错误情况。创建自定义异常通常有助于更清晰地表达程序的意图和处理程序的特定错误。

自定义异常通常通过继承Java的Exception类或其子类来创建。如果希望自定义异常是受检异常(即必须显式处理的异常),则应继承Exception类。如果希望它是非受检异常,则应继承RuntimeException。自定义异常类应该至少提供一个构造方法,它通常会调用父类的构造方法来初始化异常消息。可以根据需要提供多个构造方法,以便在抛出异常时可以传递不同类型的信息。

public class LogIn {private String userName = "admin";private String password = "123456";public static void loginInfo(String userName, String password) {if (!userName.equals(userName)) {}if (!password.equals(password)) {}System.out.println("登陆成功");}public static void main(String[] args) {loginInfo("admin", "123456");}
}

此时我们在处理用户名密码错误的时候可能就需要抛出两种异常。我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。

具体方式:

  1. 自定义异常类,然后继承自Exception 或者 RunTimeException
  2. 实现一个带有String类型参数的构造方法,参数含义:出现异常的原因
class UserNameException extends Exception {public UserNameException(String message) {super(message);}
}
class PasswordException extends Exception {public PasswordException(String message) {super(message);}
}

此时我们的 login 代码可以改成:

public class LogIn {private String userName = "admin";private String password = "123456";public static void loginInfo(String userName, String password)throws UserNameException,PasswordException{if (!userName.equals(userName)) {throw new UserNameException("用户名错误!");}if (!password.equals(password)) {throw new PasswordException("用户名错误!");}System.out.println("登陆成功");}public static void main(String[] args) {try {loginInfo("admin", "123456");} catch (UserNameException e) {e.printStackTrace();} catch (PasswordException e) {e.printStackTrace();}}
}

注意:

  1. 自定义异常通常会继承自 Exception 或者 RuntimeException
  2. 继承自 Exception 的异常默认是受查异常
  3. 继承自RuntimeException 的异常默认是非受查异常.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/604893.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leaflet:加载本地shp文件,并在地图上显示出来 (138)

第138个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载本地shp文件,利用shapefile读取shp数据,转换为json,利用L.geoJSON()在地图上显示图形。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果安装加载shapefile.j…

Docker使用扩展

日升时奋斗&#xff0c;日落时自省 目录 1、容器 1.1、容器的生命周期 1.1.1、容器OOM 1.1.2、容器异常退出 1.1.3、容器暂停 1.2、容器命令 1.2.1、创建容器 1.2.2、启动容器 1.2.3、容器日志 1.2.4、容器交互 1.2.5、容器停止 1.2.6、扩展 1.3、综合演示 2、存…

ChatGPT | 模型架构 | 应用 | 思考

介绍 ChatGPT 3.5 是 OpenAI 推出的语言模型的一个版本&#xff0c;是 GPT&#xff08;生成式预训练模型&#xff09;系列的一部分。在自然语言理解和生成方面具有强大的能力&#xff0c;可以应用于问答系统、文本生成、翻译和对话系统等多个领域。 模型架构 GPT-3.5&#x…

【代码】Keras3.0:实现残差连接

简介 残差连接是一种非常重要的网络结构创新&#xff0c;最早被广泛应用于ResNet&#xff08;Residual Neural Network&#xff09;模型中&#xff0c;由何凯明等人在2015年的论文"Deep Residual Learning for Image Recognition"中提出。 核心思想 通过引入“short…

如何查找文献及相关代码

参考文献 1.文献调研之如何查找文献及源码_在哪个网站能下载有代码的文献-CSDN博客 2.如何寻找论文及其相关代码&#xff1f;_google学术中的论文代码怎么找-CSDN博客 3.找论文代码_researchcode-CSDN博客 4. 如何最快速找到自己想要的优质的论文以及代码?一个超强大的网站…

Vue3-42-组件-给组件指定名称 + defineOptions 函数的简单使用

问题说明 当我们在 使用 <script setup> 组合式 API的时候&#xff0c;发现我们并没有给组件指定它的名称。 此时它实际上使用的是 文件的名称 作为组件的名称。 例如 &#xff1a; 组件文件是 【componentABC.vue】 那么这个组件的默认名称就是 【componentABC】.现在我…

Linux stm32串口下载程序

一、工具 使用stm32flash进行串口下载 二、stm32flash安装 sudo apt-get install stm32flash 三、查看串口设备名称 先拔掉串口运行下面指令&#xff0c;获得所有设备名称,插上串口再运行一次&#xff0c;新增的就是串口设备名称&#xff0c;记住串口设备名称&#xff0c;以…

【Hadoop】说下HDFS读文件和写文件的底层原理?

文件读取文件的写入 文件读取 客户端调用 FileSystem 对象的 open&#xff08;&#xff09;函数&#xff0c;打开想要读取的文件。其中FileSystem 是 DistributeFileSystem 的一个实例&#xff1b;DistributedFileSystem 通过使用 RPC&#xff08;远程过程调用&#xff09; 访N…

Linux rpm命令教程:如何使用rpm命令进行软件包管理(附实例详解和注意事项)

Linux rpm命令介绍 rpm命令&#xff0c;全称为Red Hat Package Manager&#xff0c;是用于管理Linux各项套件的程序。它最初是由Red Hat Linux发行版专门用来管理Linux各项套件的程序&#xff0c;由于它遵循GPL规则且功能强大方便&#xff0c;因而广受欢迎&#xff0c;逐渐受到…

简单讲述网络安全的概念、类型和重要性

什么是网络安全&#xff1f; 网络安全是指用于防止网络攻击或减轻其影响的任何技术、措施或做法。网络安全旨在保护个人和组织的系统、应用程序、计算设备、敏感数据和金融资产&#xff0c;使其免受简单而不堪其绕的计算机病毒、复杂而代价高昂的勒索软件攻击&#xff0c;以及介…

Unity 了解Input Manage下默认的输入轴

在Unity菜单Edit->Project Settings->Input Manager->Axes下有一些默认的输入轴&#xff0c;如 这些输入轴代表不同类型的输入&#xff0c;其中&#xff1a; Horizontal&#xff1a;水平移动输入轴。通常与键盘的左右箭头键、A和D键、游戏手柄的左摇杆水平轴等相关联…

【AI视野·今日CV 计算机视觉论文速览 第282期】Wed, 3 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Wed, 3 Jan 2024 Totally 70 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Street Gaussians for Modeling Dynamic Urban Scenes Authors Yunzhi Yan, Haotong Lin, Chenxu Zhou, Weijie Wang, Haiya…

nvm安装教程,实现node的多版本管理(图文界面)

目录 前言1. 安装配置2. 使用方式 前言 由于前端项目不同的node版本&#xff0c;导致重复的卸载安装会比较麻烦&#xff0c;对此需要nvm来管理 类似python版本的差异&#xff0c;可以使用虚拟环境管理&#xff08;anconda&#xff09;&#xff0c;在我原先的文章也有讲解过 …

js 如何判断对象自身为空?

js 如何判断对象自身为空&#xff1f; 前置知识&#xff1a; js 对象中的可枚举属性 enumerable &#xff1a; 用来控制所描述的属性是否将被包括在 for…in 循环之中&#xff08;除非属性名是一个 Symbol &#xff09;。具体来说&#xff0c;如果一个属性的 enumerable 为fa…

从法律和经济学角度看工业数据共享

文章目录 前言一、工业数据利用现状二、工业数据的新特点三、数据再利用和储备的现状(一)防止先行者通过数据进行垄断(二)数据储备情况以及数据成本、数量和价值四、工业数据共享的法律经济学模型分析情形一:在没有数据共享政策的情况下,尝试给出如下命题。情形二:有数据…

LeetCode-加一(66)

题目描述&#xff1a; 给定一个由整数组成的非空数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。 思路&#xff1a; 这里主要分…

《路由与交换技术》---简答题

1、什么是STP&#xff1f;解决什么问题&#xff1f; STP代表生成树协议&#xff08;Spanning Tree Protocol&#xff09;。它是用于在计算机网络中解决环路问题的一种协议。 STP的主要目标是消除环路&#xff0c;保持网络的稳定性和可靠性&#xff0c;同时提供冗余路径以实现网…

快速实现产品智能:用 AI 武装你的 API | 开源日报 No.138

openchatai/OpenCopilot Stars: 3.8k License: MIT OpenCopilot 是一个允许你拥有自己产品的 AI 副驾驶员的项目。它集成了产品底层 API&#xff0c;并可以在需要时执行 API 调用。它使用 LLMs 来确定用户请求是否需要调用 API 端点&#xff0c;然后决定调用哪个端点并根据给定…

【OJ】C++,Java,Python,Go,Rust

for循环语法 // cpp// java// python for i in range(集合): for i, val in enumerate(集合): for v1,v2,v3,... in zip(集合1,集合2,集合3,...):Pair // cpp pair<int, string> first second // java Pair<Integer, String> first() new Pair<>(firstVal…

【设计模式之美】里式替换(LSP)跟多态有何区别?如何理解LSP中子类遵守父类的约定

文章目录 一. 如何理解“里式替换原则”&#xff1f;二. 哪些代码明显违背了 LSP&#xff1f;三. 回顾 一. 如何理解“里式替换原则”&#xff1f; 子类对象能够替换程序中父类对象出现的任何地方&#xff0c;并且保证原来程序的逻辑行为不变及正确性不被破坏。 里氏替换原则…