目录
[二、Java 异常体系结构:理清继承关系,不混淆概念](#二、Java 异常体系结构:理清继承关系,不混淆概念)
[3.1 两种防御式编程思想](#3.1 两种防御式编程思想)
[LBYL:事前防御型(Look Before You Leap)](#LBYL:事前防御型(Look Before You Leap))
[EAFP:事后处理型(It's Easier to Ask Forgiveness than Permission)](#EAFP:事后处理型(It's Easier to Ask Forgiveness than Permission))
[3.2 手动抛出异常:throw](#3.2 手动抛出异常:throw)
[throw 使用注意事项](#throw 使用注意事项)
[3.3 声明异常:throws](#3.3 声明异常:throws)
[throws 使用注意事项](#throws 使用注意事项)
[3.4 捕获并处理异常:try-catch](#3.4 捕获并处理异常:try-catch)
[try-catch 使用关键注意事项](#try-catch 使用关键注意事项)
[3.5 必执行的代码块:finally](#3.5 必执行的代码块:finally)
[测试结果 1:输入正确整数](#测试结果 1:输入正确整数)
[测试结果 2:输入非整数](#测试结果 2:输入非整数)
[finally 的特殊注意事项](#finally 的特殊注意事项)
[5.1 自定义异常的实现步骤](#5.1 自定义异常的实现步骤)
[5.2 实战示例:用户登陆业务的自定义异常](#5.2 实战示例:用户登陆业务的自定义异常)
[步骤 1:实现自定义异常类](#步骤 1:实现自定义异常类)
[步骤 2:在业务代码中抛出自定义异常](#步骤 2:在业务代码中抛出自定义异常)
[步骤 3:调用业务方法,处理自定义异常](#步骤 3:调用业务方法,处理自定义异常)
[5.3 自定义异常的选型建议](#5.3 自定义异常的选型建议)
在 Java 开发的道路上,异常处理是绕不开的核心知识点。无论是新手调试代码时遇到的NullPointerException,还是开发企业级项目时处理的网络、文件 IO 异常,掌握规范的异常处理方式,能让我们的代码更健壮、更易维护,还能大幅降低线上问题的排查成本。本文将从异常的核心概念出发,逐步拆解 Java 异常体系、处理方式、执行流程,最后手把手教你实现自定义异常,让你彻底吃透 Java 异常处理。
一、什么是异常?打破程序的正常流程
异常是程序运行过程中 发生的非正常行为 / 错误状态,它会打断程序的正常执行流程,需要开发者通过特定方式进行处理。
在开发中,我们无法避免各类异常场景:比如除数为 0 的算术错误、访问数组不存在的下标、调用空对象的方法、读取不存在的文件、网络请求超时等。这些场景无法通过普通的逻辑判断完全规避,而 Java 为每一种异常场景都提供了对应的类来描述,让我们能精准定位和处理问题。
常见异常示例
java
/**
* 常见运行时异常演示
*/
public class CommonExceptionDemo {
public static void main(String[] args) {
// 1. 算术异常:ArithmeticException
int a = 10;
int b = 0;
// System.out.println(a / b);
// 2. 数组越界异常:ArrayIndexOutOfBoundsException
String[] strArr = {"Java", "Python", "C++"};
// System.out.println(strArr[5]);
// 3. 空指针异常:NullPointerException
String str = null;
// System.out.println(str.length());
// 4. 类型转换异常:ClassCastException
Object obj = new Integer(100);
// System.out.println((String) obj);
}
}
取消上述代码的注释,运行后会看到控制台抛出对应的异常信息,包含异常类型、异常原因和出错的代码行,这也是 Java 异常给我们的核心调试线索。
二、Java 异常体系结构:理清继承关系,不混淆概念
Java 为了对不同类型的异常和错误进行分类管理,设计了一套清晰的异常体系,其顶层类是java.lang.Throwable,所有异常和错误都直接或间接继承自该类。
核心继承结构
Throwable
├─ Error:JVM级别的严重错误,无法通过代码处理
│ ├─ StackOverflowError:栈溢出错误(如递归无终止条件)
│ └─ OutOfMemoryError:内存溢出错误(OOM)
└─ Exception:程序级别的异常,开发者可通过代码处理
├─ 编译时异常(受检查异常 Checked Exception):编译期必须处理
│ ├─ IOException:IO操作相关异常(如文件读取、网络请求)
│ ├─ SQLException:数据库操作相关异常
│ └─ ClassNotFoundException:类加载失败异常
└─ 运行时异常(非受检查异常 Unchecked Exception):运行期才会出现,可按需处理
├─ NullPointerException:空指针异常
├─ ArrayIndexOutOfBoundsException:数组越界异常
├─ ArithmeticException:算术异常
└─ ClassCastException:类型转换异常
三大核心类的区别
- Error :Java 虚拟机无法解决的严重问题,属于系统级错误,一旦发生程序基本无法恢复,只能提前预防。比如递归调用无终止条件会导致
StackOverflowError,创建大量对象未释放会导致OutOfMemoryError。 - 编译时异常(Checked Exception) :在程序编译阶段 就会被检测到的异常,编译器强制要求开发者必须处理(捕获或抛出),否则代码无法通过编译。比如读取文件时的
FileNotFoundException,必须显式处理。 - 运行时异常(Unchecked Exception) :继承自
RuntimeException的异常,在程序运行阶段才会触发,编译器不强制处理。这类异常通常是由于开发者的代码逻辑错误导致的,比如空指针、数组越界,建议通过优化代码逻辑避免,而非被动处理。
重要区分 :编译期的语法错误 (如把System.out.println写成system.out.println)不属于异常,只是代码书写错误,编译器会直接提示,无法生成 class 文件;而异常是代码编译通过后,JVM 执行时发生的错误。
三、异常的处理方式:从防御式编程到核心关键字
在处理异常前,我们需要了解两种编程思想:事前防御 和事后处理 ,Java 异常处理的核心基于后者,同时提供了 5 个核心关键字:throw、throws、try、catch、finally,掌握这 5 个关键字,就能处理绝大多数异常场景。
3.1 两种防御式编程思想
LBYL:事前防御型(Look Before You Leap)
在执行操作前,对所有可能出现的问题进行充分检查,正常流程和错误处理流程混在一起,代码可读性差。
java
/**
* 事前防御型编程示例:用户登陆
*/
public class LBYLDemo {
public static void main(String[] args) {
String username = "test";
String password = "123";
// 检查用户名是否为空
if (username == null || username.isEmpty()) {
System.out.println("错误:用户名为空");
return;
}
// 检查密码是否为空
if (password == null || password.isEmpty()) {
System.out.println("错误:密码为空");
return;
}
// 检查用户名密码是否正确
if (!"admin".equals(username) || !"admin123".equals(password)) {
System.out.println("错误:用户名或密码错误");
return;
}
System.out.println("登陆成功");
}
}
缺陷 :代码中大量的if判断让核心业务逻辑被淹没,后期维护难度大。
EAFP:事后处理型(It's Easier to Ask Forgiveness than Permission)
先执行操作,遇到问题再捕获处理,将正常流程和错误流程分离,代码更清晰,这也是 Java 异常处理的核心思想。
java
/**
* 事后处理型编程示例:用户登陆
*/
public class EAFPDemo {
public static void main(String[] args) {
String username = "test";
String password = "123";
try {
// 直接执行核心业务逻辑,不做前置检查
login(username, password);
System.out.println("登陆成功");
} catch (NullPointerException e) {
System.out.println("错误:用户名或密码为空");
} catch (IllegalArgumentException e) {
System.out.println("错误:" + e.getMessage());
}
}
private static void login(String username, String password) {
if (username == null || password == null) {
throw new NullPointerException();
}
if (!"admin".equals(username) || !"admin123".equals(password)) {
throw new IllegalArgumentException("用户名或密码错误");
}
}
}
优势 :核心业务逻辑login()方法简洁,错误处理集中在catch块,开发者更关注正常流程,代码可读性和可维护性大幅提升。
3.2 手动抛出异常:throw
在编写程序时,如果检测到非法的业务逻辑或参数错误 ,需要主动将错误信息告知调用者,此时可以使用throw关键字手动抛出一个指定的异常对象。
语法格式
java
throw new 异常类名("异常产生的原因");
实战示例:参数合法性校验
java
/**
* throw 手动抛出异常示例:获取集合指定下标元素
*/
import java.util.List;
public class ThrowDemo {
public static <T> T getListElement(List<T> list, int index) {
// 校验集合是否为null
if (list == null) {
throw new NullPointerException("传递的集合对象为null,无法获取元素");
}
// 校验下标是否合法
if (index < 0 || index >= list.size()) {
throw new IndexOutOfBoundsException("传递的下标" + index + "越界,集合长度为" + list.size());
}
// 校验通过,返回元素
return list.get(index);
}
public static void main(String[] args) {
List<String> list = List.of("Java", "异常", "处理");
// 正常获取
System.out.println(getListElement(list, 1));
// 下标越界,手动抛出异常
// System.out.println(getListElement(list, 5));
// 集合为null,手动抛出异常
// System.out.println(getListElement(null, 0));
}
}
throw 使用注意事项
throw必须写在方法体内部;- 抛出的对象必须是
Exception或其子类的实例; - 抛出运行时异常 (如
NullPointerException),调用者可按需处理,编译器不强制; - 抛出编译时异常 (如
IOException),调用者必须处理(捕获或抛出),否则代码无法编译; - 异常一旦抛出,其后的代码将不会执行。
3.3 声明异常:throws
如果当前方法没有能力处理 抛出的异常,或者希望将异常处理的责任转移给调用者,此时可以使用throws关键字在方法声明处声明该方法可能抛出的异常。
语法格式
java
修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2... {
// 方法体,可能抛出异常
}
实战示例:文件操作声明编译时异常
java
/**
* throws 声明异常示例:文件读取
*/
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
public class ThrowsDemo {
// 声明文件未找到异常,交给调用者处理
public static FileReader openFile(String filePath) throws FileNotFoundException {
File file = new File(filePath);
// FileNotFoundException是编译时异常,此处不处理,声明后抛出
return new FileReader(file);
}
public static void main(String[] args) {
try {
// 调用声明异常的方法,必须处理异常
FileReader fr = openFile("test.txt");
System.out.println("文件打开成功");
fr.close();
} catch (FileNotFoundException e) {
System.out.println("异常原因:" + e.getMessage());
}
}
}
throws 使用注意事项
throws必须跟在方法参数列表之后;- 声明的异常必须是
Exception或其子类; - 方法内部抛出多个异常时,
throws后用逗号 分隔多个异常类型;若异常之间有父子关系,直接声明父类异常 即可(如FileNotFoundException继承自IOException,可直接声明throws IOException); - 调用声明了编译时异常 的方法,调用者必须处理(
try-catch捕获或继续throws抛出);调用声明了运行时异常的方法,编译器不强制处理。
3.4 捕获并处理异常:try-catch
throws只是将异常转移给调用者,并未真正处理异常;而try-catch是 Java 中处理异常的核心方式,能捕获异常并对其进行处理,让程序在发生异常后继续执行。
语法格式
java
try {
// 可能抛出异常的代码块(监控区)
} catch (异常类型1 异常对象名) {
// 处理异常类型1的代码(捕获区)
} catch (异常类型2 异常对象名) {
// 处理异常类型2的代码
}
// 可选:finally块,下文单独讲解
实战示例:多异常捕获与处理
java
/**
* try-catch 捕获异常示例:多异常处理
*/
public class TryCatchDemo {
public static void calculate(int a, int b, int[] arr) {
try {
System.out.println("a / b = " + (a / b));
System.out.println("数组下标0的元素:" + arr[0]);
} catch (ArithmeticException e) {
// 处理算术异常
System.out.println("处理算术异常:" + e.getMessage());
} catch (NullPointerException e) {
// 处理空指针异常
System.out.println("处理空指针异常:数组对象为null");
} catch (ArrayIndexOutOfBoundsException e) {
// 处理数组越界异常
System.out.println("处理数组越界异常:" + e.getMessage());
}
}
public static void main(String[] args) {
// 测试1:除数为0
calculate(10, 0, new int[]{1,2});
System.out.println("===== 分割线 =====");
// 测试2:数组为null
calculate(10, 2, null);
System.out.println("===== 分割线 =====");
// 测试3:数组越界(空数组)
calculate(10, 2, new int[]{});
// 异常处理后,后续代码正常执行
System.out.println("程序执行完成");
}
}
运行结果
处理算术异常:/ by zero
===== 分割线 =====
处理空指针异常:数组对象为null
===== 分割线 =====
处理数组越界异常:Index 0 out of bounds for length 0
程序执行完成
可以看到,即使发生了异常,经过try-catch处理后,程序的后续代码依然能正常执行,这也是异常处理的核心目的。
try-catch 使用关键注意事项
-
try块中抛出异常的位置后续代码不会执行; -
异常捕获遵循类型匹配原则 :只有
catch的异常类型与try中抛出的异常类型一致 ,或为其父类,才能捕获到异常; -
处理多个不同类型的异常 时,需注意子类异常在前,父类异常在后 ,否则会出现语法错误(父类异常会捕获所有子类异常,后续的子类异常
catch块永远无法执行); -
若多个异常的处理逻辑完全相同 ,可使用 ** 竖线 |** 合并捕获,简化代码:
javacatch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) { System.out.println("处理异常:" + e.getMessage()); } -
可以使用
Exception捕获所有异常 (因为Exception是所有程序级异常的父类),但不推荐:会掩盖具体的异常类型,不利于问题排查,仅适用于通用的异常兜底处理。
3.5 必执行的代码块:finally
在程序开发中,有些代码无论是否发生异常,都必须执行 ,比如打开的文件流、数据库连接、网络连接等资源的释放,否则会造成资源泄漏 。finally块就是为了解决这个问题,它配合try-catch使用,里面的代码永远会被执行。
语法格式
java
try {
// 可能抛出异常的代码
} catch (异常类型 e) {
// 处理异常的代码
} finally {
// 无论是否发生异常,都会执行的代码(资源释放为主)
}
核心场景:资源释放(全新示例)
java
/**
* finally 块示例:资源释放(Scanner)
*/
import java.util.Scanner;
import java.util.InputMismatchException;
public class FinallyDemo {
public static int getIntInput() {
Scanner sc = new Scanner(System.in);
try {
System.out.print("请输入一个整数:");
// 尝试获取整数输入
int num = sc.nextInt();
return num;
} catch (InputMismatchException e) {
System.out.println("输入类型错误,不是整数");
return -1;
} finally {
// 无论是否输入正确,都关闭Scanner,释放资源
System.out.println("执行finally块:关闭Scanner资源");
sc.close();
}
}
public static void main(String[] args) {
int num = getIntInput();
System.out.println("获取到的数字:" + num);
}
}
测试结果 1:输入正确整数
请输入一个整数:100
执行finally块:关闭Scanner资源
获取到的数字:100
测试结果 2:输入非整数
请输入一个整数:abc
输入类型错误,不是整数
执行finally块:关闭Scanner资源
获取到的数字:-1
可以看到,无论try块中是否发生异常,finally块的代码都会执行,完美解决了资源释放的问题。
finally 的特殊注意事项
finally块的执行时机:在方法返回之前 (即使try或catch中有return语句,也会先执行finally块,再执行return);- 若
finally块中也有return语句,会覆盖try或catch中的return结果,强烈不建议 在finally中写return(编译器会给出警告); finally块唯一不执行的情况:程序执行到try/catch块时,调用了System.exit(0)(强制终止 JVM),此时 JVM 直接退出,所有代码都不再执行。
四、异常的处理流程:跟着调用栈走,理清执行顺序
要彻底理解异常处理,必须理清异常的传播和处理流程 ,而核心就是方法调用栈 :Java 中方法之间的调用关系会被 JVM 存储在虚拟机栈 中,当发生异常时,异常会沿着调用栈从下往上传播,直到被捕获处理,若最终无人处理,则由 JVM 接管,程序异常终止。
异常处理完整执行流程
- 程序先执行
try块中的代码; - 若
try块中未发生异常 ,跳过catch块,直接执行finally块,再执行try-catch-finally后的代码; - 若
try块中发生异常 ,立即终止try块后续代码,匹配catch块的异常类型:- 找到匹配的异常类型 :执行对应
catch块的处理代码,再执行finally块,最后执行后续代码; - 未找到匹配的异常类型 :先执行
finally块,再将异常向上传播给上层调用者;
- 找到匹配的异常类型 :执行对应
- 上层调用者重复步骤 3,若所有调用者都未处理异常,最终传递到
main方法; - 若
main方法也未处理异常,异常会被JVM 接管 ,JVM 会打印异常信息(类型、原因、调用栈),并强制终止程序,main方法后续代码不再执行。
实战示例:异常的向上传播
java
/**
* 异常处理流程示例:异常向上传播
*/
public class ExceptionFlowDemo {
// 方法3:抛出数组越界异常
public static void method3() {
int[] arr = {1,2,3};
System.out.println(arr[10]); // 抛出异常
}
// 方法2:调用method3,未处理异常
public static void method2() {
method3();
}
// 方法1:调用method2,捕获并处理异常
public static void method1() {
try {
method2();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("method1捕获到异常:" + e.getMessage());
}
}
public static void main(String[] args) {
method1();
// 异常被处理,后续代码正常执行
System.out.println("main方法后续代码执行");
}
}
运行结果
java
method1捕获到异常:Index 10 out of bounds for length 3
main方法后续代码执行
若删除method1中的try-catch,异常会传播到main方法,若main方法也不处理,JVM 会接管,程序终止,main方法后续代码不再执行。
五、自定义异常:贴合业务场景,让异常更有意义
Java 内置了丰富的异常类,但这些异常类都是通用的 ,无法精准描述实际开发中的业务异常 ,比如用户登陆时的 "用户名不存在"、"密码错误",订单操作时的 "订单不存在"、"库存不足" 等。此时我们需要自定义异常类,贴合业务场景,让异常信息更精准,便于问题排查和业务处理。
5.1 自定义异常的实现步骤
Java 中自定义异常的核心是继承,遵循以下两步即可:
- 自定义异常类,继承自
Exception(编译时异常,受检查)或RuntimeException(运行时异常,非受检查); - 实现带 String 类型参数的构造方法 ,将异常原因通过
super()传递给父类构造方法(便于通过getMessage()获取异常原因)。
5.2 实战示例:用户登陆业务的自定义异常
我们针对用户登陆场景,自定义两个业务异常:UserNameNotExistException(用户名不存在)、PasswordErrorException(密码错误),并在业务代码中抛出和处理。
步骤 1:实现自定义异常类
java
/**
* 自定义异常:用户名不存在(继承Exception,编译时异常)
*/
public class UserNameNotExistException extends Exception {
// 构造方法,传递异常原因
public UserNameNotExistException(String message) {
super(message);
}
}
/**
* 自定义异常:密码错误(继承Exception,编译时异常)
*/
public class PasswordErrorException extends Exception {
public PasswordErrorException(String message) {
super(message);
}
}
可选优化 :若希望自定义异常为运行时异常,只需将父类改为RuntimeException,编译器不强制处理。
步骤 2:在业务代码中抛出自定义异常
java
/**
* 用户登陆业务类
*/
public class UserLoginService {
// 模拟数据库中的用户信息
private static final String DB_USERNAME = "admin";
private static final String DB_PASSWORD = "admin123456";
/**
* 登陆方法,抛出自定义业务异常
* @param username 用户名
* @param password 密码
* @throws UserNameNotExistException 用户名不存在
* @throws PasswordErrorException 密码错误
*/
public void login(String username, String password) throws UserNameNotExistException, PasswordErrorException {
// 校验用户名
if (!DB_USERNAME.equals(username)) {
throw new UserNameNotExistException("用户名[" + username + "]不存在");
}
// 校验密码
if (!DB_PASSWORD.equals(password)) {
throw new PasswordErrorException("密码错误,请重新输入");
}
}
}
步骤 3:调用业务方法,处理自定义异常
java
/**
* 测试自定义异常:用户登陆
*/
public class CustomExceptionTest {
public static void main(String[] args) {
UserLoginService loginService = new UserLoginService();
// 测试1:用户名不存在
String username = "test";
String password = "admin123456";
try {
loginService.login(username, password);
System.out.println("登陆成功!");
} catch (UserNameNotExistException e) {
System.out.println("登陆失败:" + e.getMessage());
// 可做后续处理,如跳转到注册页面
} catch (PasswordErrorException e) {
System.out.println("登陆失败:" + e.getMessage());
// 可做后续处理,如提示密码找回
}
// 测试2:密码错误
System.out.println("===== 分割线 =====");
username = "admin";
password = "123";
try {
loginService.login(username, password);
System.out.println("登陆成功!");
} catch (UserNameNotExistException | PasswordErrorException e) {
System.out.println("登陆失败:" + e.getMessage());
}
}
}
运行结果
登陆失败:用户名[test]不存在
===== 分割线 =====
登陆失败:密码错误,请重新输入
可以看到,自定义异常能精准描述业务中的错误场景,让异常处理更贴合实际业务,同时异常信息更直观,便于开发和运维人员排查问题。
5.3 自定义异常的选型建议
- 若希望编译器强制处理 该异常(如核心业务异常,必须显式处理),让自定义异常继承
Exception(编译时异常); - 若该异常可通过代码逻辑避免 ,或希望简化代码(不强制处理),让自定义异常继承
RuntimeException(运行时异常); - 自定义异常的命名要见名知意 ,通常以
Exception结尾,如OrderNotExistException、StockNotEnoughException。
六、异常处理的最佳实践
掌握了异常的基础知识点后,更重要的是在实际开发中遵循最佳实践,让异常处理更规范、更高效:
- 避免捕获所有异常 :不要直接捕获
Exception,会掩盖具体的异常类型,不利于问题排查,应捕获具体的异常类型; - 不要忽略异常 :不要在
catch块中只写e.printStackTrace(),甚至空的catch块,应根据业务场景做具体处理(如记录日志、提示用户、重试操作); - 及时释放资源 :打开的 IO 流、数据库连接、网络连接等资源,必须在
finally块中释放,或使用 Java7 的try-with-resources自动释放; - 异常信息要精准 :抛出异常时,填写清晰的异常原因(如
throw new NullPointerException("用户信息对象为null,无法获取用户ID")),便于排查问题; - 子类方法抛出异常范围不超过父类:继承父类并重写方法时,子类方法抛出的异常类型不能是父类方法异常的父类,也不能抛出更多的受检查异常;
- 合理选择自定义异常的父类 :核心业务异常建议继承
Exception,强制调用者处理;非核心异常建议继承RuntimeException,简化代码; - 使用日志框架记录异常 :实际开发中,不要使用
e.printStackTrace(),应使用 SLF4J/Logback 等日志框架记录异常(可记录异常级别、调用栈、业务上下文),便于线上问题排查。
七、总结
Java 异常处理是保证程序健壮性的核心,其核心是事后处理 的编程思想,通过throw、throws、try、catch、finally五个关键字实现异常的抛出、声明、捕获和处理。
本文从异常的概念出发,理清了Throwable、Error、Exception的继承关系,区分了编译时异常和运行时异常;然后详细讲解了异常处理的核心方式和执行流程;最后通过实战实现了贴合业务的自定义异常,并给出了开发中的最佳实践。
掌握异常处理的关键,不仅是记住语法和规则,更重要的是结合业务场景选择合适的处理方式:让程序在发生异常时,既能精准定位问题,又能优雅地处理异常,保证程序的正常运行。希望本文能让你对 Java 异常处理有更全面、更深入的理解,在实际开发中玩转异常体系!