目录
一:异常的概念与体系结构
1.异常的概念
2.异常的体系结构
3.异常的分类
1.编译时异常
2.运⾏时异常
二:异常的处理
1.防御式编程
2.异常的抛出-throw
3.异常的声明-throws
4.异常的捕获-try-catch捕获并处理异常
5.finally
6.异常的处理流程
三:⾃定义异常类
1.实现⾃定义异常
内容大纲:
本文全面介绍了Java异常处理机制,主要内容包括:
- 防御式编程的两种模式(LBYL事前防御和EAFP事后处理)
- 5个核心关键字:throw(抛出异常)、throws(声明异常)、try-catch(捕获处理)、finally(资源清理)
文章通过代码示例详细演示了异常处理的实际应用,包括常见异常类型、异常捕获的优先级、finally块的作用等关键知识点,并比较了不同异常处理方式的优缺点。最后通过登录验证案例展示了自定义异常的实现方法。
一:异常的概念与体系结构
1.异常的概念
异常是在程序执⾏过程中发⽣的⼀种特殊情况或错误状态。它打断了程序的正常流程,需要特别处 理。
在⽇常开发中,程序员绞尽脑汁将代码写的尽善尽美,在程序运⾏过程中,难免会出现⼀些奇奇怪怪 的问题。有时通过代码很难去控制,⽐如:数据格式不对、⽹络不通畅、内存报警等。
在Java中,将程序执⾏过程中发⽣的不正常⾏为称为异常,⽐如之前写代码时经常遇到的:
1.算术异常
public static void main(String[] args) {
System.out.println(10/0);
}
输出:

2.数组越界异常
public static void main(String[] args) {
int[] arry={1,2,3};
System.out.println(arry[12]);
}
输出:

3.空指针异常
public static void main(String[] args) {
int[]arry=null;
System.out.println(arry.length);
}
输出: 
从上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进⾏描述
2.异常的体系结构
异常种类繁多,为了对不同异常或者错误进⾏很好的分类管理,Java内部维护了⼀个异常的体系结 构:

从上图中可以看到:
1. Throwable:是异常体系的顶层类,其派⽣出两个重要的⼦类,Error和Exception
2. Error:指的是Java虚拟机⽆法解决的严重问题,⽐如:JVM的内部错误、资源耗尽等,典型代 表:StackOverflowError和OutOfMemoryError,⼀旦发⽣回⼒乏术
3. Exception:异常产⽣后程序员可以通过代码进⾏处理,使程序继续执⾏。⽐如:感冒、发烧。我们平时所说的异常就是Exception。
3.异常的分类
异常可能在编译时发⽣,也可能在程序运⾏时发⽣,根据发⽣的时机不同,可以将异常分为:
1.编译时异常
在程序编译期间发⽣的异常,称为编译时异常,也称为受检查异常(CheckedException)
class Person{
private String name;
private String gender;
int age;
@Override
protected Object clone() {
return (Person)super.clone();
}
}
输出: 
2.运⾏时异常
在程序执⾏期间发⽣的异常,称为运⾏时异常,也称为⾮受检查异常(UncheckedException)
RunTimeException以及其⼦类对应的异常,都称为运⾏时异常。⽐如:NullPointerException、 ArrayIndexOutOfBoundsException、ArithmeticException。
注意:编译时出现的语法性错误,不能称之为异常。例如将System.out.println拼写错了,写成了 system.out.println. 此时编译过程中就会出错,这是"编译期"出错。⽽运⾏时指的是程序已经编译通 过得到class⽂件了,再由JVM执⾏过程中出现的错误.
二:异常的处理
1.防御式编程
错误在代码中是客观存在的.因此我们要让程序出现问题的时候及时通知程序猿.主要的⽅式
LBYL:LookBeforeYouLeap.在操作之前就做充分的检查. 即:事前防御型
boolean ret = false;
// 1. 尝试登陆游戏
ret = loginGame();
if (!ret) {
handleLoginError();
return;
}
// 2. 开始匹配
ret = startMatchmaking();
if (!ret) {
handleMatchmakingError();
return;
}
// 3. 游戏确认
ret = confirmGame();
if (!ret) {
handleConfirmationError();
return;
}
// 4. 选择英雄
ret = selectHero();
if (!ret) {
handleHeroSelectionError();
return;
}
// 5. 载入游戏画面
ret = loadGameScene();
if (!ret) {
handleLoadError();
return;
}
// …… 后续逻辑
缺陷:正常流程和错误处理流程代码混在⼀起,代码整体显的⽐较混乱。
EAFP:It's Easier to Ask Forgiveness than Permission. "事后获取原谅⽐事前获取许可更容易".也 就是先操作,遇到问题再处理.即:事后认错型
try {
// 尝试执行一系列可能失败的操作
loginGame(); // 登陆游戏
startMatchmaking(); // 开始匹配
confirmGame(); // 游戏确认
selectHero(); // 选择英雄
loadGameScene(); // 载入游戏画面
// … 后续流程
} catch (LoginException e) {
// 捕获并处理登陆阶段的异常
handleLoginError(e);
} catch (MatchmakingException e) {
// 捕获并处理匹配阶段的异常
handleMatchmakingError(e);
} catch (ConfirmationException e) {
// 捕获并处理确认阶段的异常
handleConfirmationError(e);
} catch (HeroSelectionException e) {
// 捕获并处理选英雄阶段的异常
handleHeroSelectionError(e);
} catch (LoadSceneException e) {
// 捕获并处理加载阶段的异常
handleLoadSceneError(e);
}
// ……
优势:正常流程和错误流程是分离开的,程序员更关注正常流程,代码更清晰,容易理解代码
异常处理的核⼼思想就是EAFP。
在Java中,异常处理主要的5个关键字:throw、try、catch、finally、throws。
2.异常的抛出-throw
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调⽤者,⽐如:参数检测。
在Java中,可以借助throw关键字,抛出⼀个指定的异常对象,将错误信息告知给调⽤者。具体语法 如下:
throw new XXXException(" 异常产⽣的原因 ");
【需求】:实现⼀个获取数组中任意位置元素的⽅法。
class ExceptionDemo {
/**
* 获取数组元素的方法,包含防御性编程检查
*/
public static int getElement(int[] array, int index) {
// 1. 检查数组是否为空
if (null == array) {
throw new NullPointerException("传递的数组为null");
}
// 2. 检查下标是否越界
if (index < 0 || index >= array.length) {
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
// 3. 正常返回元素
return array[index];
}
public static void main(String[] args) {
int[] array = {1, 2, 3};
// 调用方法,传入下标 3
// 注意:数组长度为3,有效下标是 0, 1, 2。
// 传入 3 会导致 index >= array.length 成立,从而抛出异常
getElement(array, 3);
}
}
输出:

【注意事项】
1. throw必须写在⽅法体内部
2. 抛出的对象必须是Exception或者Exception的⼦类对象
3. 如果抛出的是RunTimeException或者RunTimeException的⼦类,则可以不⽤处理,直接交给 JVM来处理
4. 如果抛出的是编译时异常,⽤⼾必须处理,否则⽆法通过编译
5. 异常⼀旦抛出,其后的代码就不会执⾏
3.异常的声明-throws
throws 关键字⽤于在⽅法声明中列出该⽅法可能抛出的异常,它告诉调⽤者这个⽅法可能会抛出某 些异常,调⽤者需要处理这些异常。使⽤ throws 实际上是将异常的处理责任转移给了调⽤该⽅法 的代码
语法格式:
修饰符 返回值类型 ⽅法名 ( 参数列表 ) throws 异常类型 1 ,异常类型 2 …{
}
【需求】:加载指定的配置⽂件config.ini
import java.io.File;
import java.io.FileNotFoundException;
public class Config {
File file;
/**
* 打开配置文件的方法
*
* FileNotFoundException 是编译时异常(受检异常)。
* 此处方法内部没有能力处理文件不存在的问题,因此通过 throws 关键字
* 将异常抛给调用者,让调用者去检查文件名是否给错了。
*/
public void OpenConfig(String filename) throws FileNotFoundException {
// 模拟逻辑:如果文件名是 "config.ini",则人为抛出一个文件未找到的异常
if (filename.equals("config.ini")) {
throw new FileNotFoundException("配置文件名字不对");
}
// 打开文件的其他逻辑…
}
public void readConfig() {
// 读取配置的逻辑…
}
}
【注意事项】
1. throws必须跟在⽅法的参数列表之后
2. 声明的异常必须是Exception或者Exception的⼦类
3. ⽅法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间⽤逗号隔开,如果抛出多个 异常类型具有⽗⼦关系,直接声明⽗类即可。
4. 调⽤声明抛出异常的⽅法时,如果该异常是编译时异常/受查异常时,调⽤者必须对该异常进⾏处 理,或者继续使⽤throws抛出
4.异常的捕获-try-catch捕获并处理异常
throws对异常并没有真正处理,⽽是将异常报告给抛出异常⽅法的调⽤者,由调⽤者处理。如果真正 要对异常进⾏处理,就需要try-catch。
public class ExceptionSyntaxDemo {
public void executeLogic() {
try {
// 【可能出现异常的代码】
// 这里放置可能会抛出异常的逻辑(例如文件读取、网络请求、数组访问等)
System.out.println("尝试执行可能出错的代码…");
} catch (SpecificException e) {
// 【捕获特定异常】
// 如果 try 中抛出的异常类型与这里的 SpecificException 一致(或是其子类),则进入此块
System.out.println("捕获到特定异常:" + e.getMessage());
} catch (Exception e) {
// 【捕获通用异常 – 可选】
// 如果上面的 catch 没匹配到,但异常是 Exception 的子类,则进入此块
// 注意:多个 catch 块时,父类异常必须放在子类异常后面
System.out.println("捕获到其他异常:" + e.getMessage());
} finally {
// 【最终执行块 – 可选】
// 无论是否发生异常,也无论异常是否被捕获,这里的代码一定会被执行
// 通常用于释放资源(如关闭文件流、数据库连接)
System.out.println("资源清理或收尾工作…");
}
// 【后序代码】
// 1. 如果异常被成功捕获(catch 匹配到了),程序会跳过 try 中剩余的代码,
// 执行 catch 块,然后执行 finally 块,最后继续执行这里的代码。
// 2. 如果异常没有被捕获(没有匹配的 catch),程序在执行完 finally 后会直接终止或向上层抛出。
System.out.println("后续业务逻辑…");
}
}
关于异常的处理⽅式
异常的种类有很多,我们要根据不同的业务场景来决定.
1. 对于⽐较严重的问题(例如和算钱相关的场景),应该让程序直接崩溃,防⽌造成更严重的后果
2. 对于不太严重的问题(⼤多数场景),可以记录错误⽇志,并通过监控报警程序及时通知程序猿
3. 对于可能会恢复的问题(和⽹络相关的场景),可以尝试进⾏重试
在我们当前的代码中采取的是经过简化的第⼆种⽅式.我们记录的错误⽇志是出现异常的⽅法调⽤信 息,能很快速的让我们找到出现异常的位置.以后在实际⼯作中我们会采取更完备的⽅式来记录异常 信息
【注意事项】
1. try块内抛出异常位置之后的代码将不会被执⾏
2. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往 外抛,直到JVM收到后中断程序—-异常是按照类型来捕获的
3. try中可能会抛出多个不同的异常对象,则必须⽤多个catch来捕获—-即多种异常,多次捕获
public class ExceptionDemo {
public static void main(String[] args) {
// 1. 初始化一个长度为3的数组
int[] array = {1, 2, 3};
try {
System.out.println("before");
// 模拟可能出错的代码(当前状态)
// 注意:array 长度为3,访问下标 100 会触发 ArrayIndexOutOfBoundsException
System.out.println(array[100]);
// 这行代码在发生异常时不会被执行
System.out.println("after");
/*
* 备选测试场景:
* 如果取消下面注释并注释掉上面的 array[100],
* 则会触发 NullPointerException
*/
// array = null;
// System.out.println(array[0]);
} catch (ArrayIndexOutOfBoundsException e) {
// 2. 捕获数组下标越界异常
System.out.println("这是个数组下标越界异常");
e.printStackTrace(); // 打印详细的错误堆栈信息
} catch (NullPointerException e) {
// 3. 捕获空指针异常
System.out.println("这是个空指针异常");
e.printStackTrace();
}
// 4. 无论是否发生异常,只要程序未崩溃退出,后续代码都会执行
System.out.println("after try catch");
}
}
4. 可以通过⼀个catch捕获所有的异常,即多个异常,⼀次捕获(不推荐)
5.finally
在写程序时,有些特定的代码,不论程序是否发⽣异常,都需要执⾏,⽐如程序中打开的资源:⽹络 连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进⾏回收。另外,因为异 常会引发程序的跳转,可能导致有些语句执⾏不到,finally就是⽤来解决这个问题的
public class FinallyDemo {
public static void main(String[] args) {
try {
// 1. 定义一个长度为3的数组
int[] array = {1, 2, 3};
// 2. 尝试访问下标为100的元素,这会导致 ArrayIndexOutOfBoundsException
// 注意:一旦这里出错,下面的 array[0] = 10; 将不会被执行
array[100] = 10;
// 3. 正常赋值逻辑(在上面的异常发生时会被跳过)
array[0] = 10;
} catch (ArrayIndexOutOfBoundsException e) {
// 4. 捕获异常并打印异常信息
System.out.println(e);
} finally {
// 5. 无论是否发生异常,这里的代码都会执行
// 常用于关闭文件流、数据库连接等清理工作
System.out.println("finally中的代码一定会执行");
}
// 6. try-catch 结构结束后的后续代码
// 因为异常已经被 catch 捕获处理了,所以程序不会崩溃,会继续执行这里
System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
}
}
注意:finally中的代码⼀定会执⾏的,⼀般在finally中进⾏⼀些资源清理的扫尾⼯作。
6.异常的处理流程
关于"调⽤栈"
⽅法之间是存在相互调⽤关系的,这种调⽤关系我们可以⽤"调⽤栈"来描述.在JVM中有⼀块内存空 间称为"虚拟机栈"专⻔存储⽅法之间的调⽤关系.当代码中出现异常的时候,我们就可以使⽤ e.printStackTrace(); 的⽅式查看出现异常代码的调⽤栈
如果本⽅法中没有合适的处理异常的⽅式,就会沿着调⽤栈向上传递
public class ExceptionPropagationDemo {
public static void main(String[] args) {
try {
// 1. 调用 func() 方法
// 因为 func() 可能会抛出异常,所以放在 try 块中进行监控
func();
} catch (ArrayIndexOutOfBoundsException e) {
// 2. 捕获从 func() 传递过来的数组下标越界异常
System.out.println("在 main 方法中捕获到了异常!");
e.printStackTrace(); // 打印详细的错误堆栈信息
}
// 3. 异常被处理后,程序继续向下执行
System.out.println("after try catch");
}
/**
* 模拟一个可能产生异常的方法
*/
public static void func() {
int[] array = {1, 2, 3};
// 尝试访问下标 100,这超出了数组长度(0-2)
// 这里会抛出 ArrayIndexOutOfBoundsException
// 由于 func() 内部没有 try-catch,异常会自动抛给调用者(main)
System.out.println(array[100]);
}
}
输出:

【异常处理流程总结】
• 程序先执⾏try中的代码
• 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配.
• 如果找到匹配的异常类型,就会执⾏catch中的代码
• 如果没有找到匹配的异常类型,就会将异常向上传递到上层调⽤者.
• ⽆论是否找到匹配的异常类型,finally中的代码都会被执⾏到(在该⽅法结束之前执⾏).
• 如果上层调⽤者也没有处理的了异常,就继续向上传递.
• ⼀直到main⽅法也没有合适的代码处理异常,就会交给JVM来进⾏处理,此时程序就会异常终⽌
三:⾃定义异常类
java 中虽然已经内置了丰富的异常类,但是并不能完全表⽰实际开发中所遇到的⼀些异常,此时就需 要维护符合我们实际情况的异常结构.
例如,我们实现⼀个⽤⼾登陆功能
public class LogIn {
// 定义系统预设的正确用户名
private String userName = "admin";
// 定义系统预设的正确密码
private String password = "123456";
/**
* 登录验证方法
* @param userName 用户输入的用户名
* @param password 用户输入的密码
*/
public void loginInfo(String userName, String password) {
// 1. 校验用户名 (使用 this.userName 区分成员变量和参数)
if (!this.userName.equals(userName)) {
System.out.println("用户名错误!");
return; // 如果用户名不对,直接结束方法,不再校验密码
}
// 2. 校验密码
if (!this.password.equals(password)) {
System.out.println("密码错误!");
return; // 如果密码不对,直接结束方法
}
// 3. 如果以上检查都通过,说明登录成功
System.out.println("登陆成功");
}
public static void main(String[] args) {
// 创建 LogIn 对象
LogIn logIn = new LogIn();
// 调用登录方法进行测试
// 这里传入的是 "admin111",与预设的 "admin" 不符
logIn.loginInfo("admin111", "123456");
}
}
输出:

此时我们在处理⽤⼾名密码错误的时候可能就需要抛出两种异常.我们可以基于已有的异常类进⾏扩展 (继承),创建和我们业务相关的异常类.
1.实现⾃定义异常
具体⽅式:
1.⾃定义异常类,然后继承⾃Exception或者RunTimeException
2.实现⼀个带有String类型参数的构造⽅法,参数含义:出现异常的原因
/**
* 自定义用户名异常
* 继承自 Exception,属于受检异常(Checked Exception)
*/
class UserNameException extends Exception {
// 构造方法,接收一个字符串作为异常信息
public UserNameException(String message) {
// 调用父类 Exception 的构造方法,将错误信息传递上去
super(message);
}
}
/**
* 自定义密码异常
*/
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
// 测试类(为了演示如何使用上面定义的异常)
public class CustomExceptionDemo {
public static void main(String[] args) {
try {
// 模拟抛出异常
throw new UserNameException("用户名不能为空!");
} catch (UserNameException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
}
}
注意事项
• ⾃定义异常通常会继承⾃Exception或者RuntimeException
• 继承⾃Exception的异常默认是受查异常
• 继承⾃RuntimeException的异常默认是⾮受查异常
以上就是我们的全部内容了!!!!




