Java 异常处理:原理与实战最佳实践
系统讲解了 Java 异常处理机制,涵盖异常分类(Error、Checked Exception、Unchecked Exception)、核心语法(try-catch-finally、throws、throw)及进阶特性(try-with-resources、异常链)。通过自定义异常示例和统一异常处理框架实战,阐述了异常处理的最佳实践与常见误区,帮助开发者编写更健壮的代码。

系统讲解了 Java 异常处理机制,涵盖异常分类(Error、Checked Exception、Unchecked Exception)、核心语法(try-catch-finally、throws、throw)及进阶特性(try-with-resources、异常链)。通过自定义异常示例和统一异常处理框架实战,阐述了异常处理的最佳实践与常见误区,帮助开发者编写更健壮的代码。

掌握异常的分类与核心概念,理解异常处理的设计思想。
熟练运用 try-catch-finally、throws、throw 处理异常。
掌握自定义异常的编写与使用场景,规范异常处理流程。
本章重点是 异常处理的最佳实践 和 避免常见误区,这是提升代码健壮性的核心技能。
异常是指程序运行过程中出现的非正常情况,它会中断程序的正常执行流程。
比如文件找不到、数组下标越界、空指针访问等,这些情况都会触发异常。
Java 中所有异常都是 Throwable 类的子类,异常处理的本质是捕获并处理这些非正常情况,保证程序可以继续运行或优雅退出。
Java 中的异常体系分为三大类,它们的父类都是 Throwable:
Error(错误):是 JVM 内部的严重错误,属于不可恢复的异常,程序无法处理。
OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出)。Checked Exception(受检异常):也称编译时异常,编译器会强制要求程序处理这类异常。
IOException(IO 异常)、SQLException(数据库异常)。try-catch 捕获,要么用 throws 声明抛出。Unchecked Exception(非受检异常):也称运行时异常,继承自 RuntimeException,编译器不强制处理。
NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组下标越界)、ClassCastException(类型转换异常)。public class ErrorDemo {
public static void recursion() {
recursion(); // 无限递归
}
public static void main(String[] args) {
recursion(); // 抛出 StackOverflowError
}
}
✅ 核心结论:异常体系的继承关系为 Throwable → Error / Exception → RuntimeException。
| 异常类型 | 产生原因 |
|---|---|
NullPointerException | 调用了 null 对象的方法或访问了 null 对象的属性 |
ArrayIndexOutOfBoundsException | 数组下标超出了数组的长度范围 |
ClassCastException | 强制类型转换失败,比如将 String 转为 Integer |
ArithmeticException | 算术运算错误,比如整数除以 0 |
IOException | 文件读写失败,比如文件不存在或权限不足 |
try-catch 是最基础的异常处理结构,try 块包裹可能抛出异常的代码,catch 块捕获并处理对应的异常。
try {
// 可能抛出异常的代码
} catch (异常类型 变量名) {
// 处理异常类型的逻辑
}
以空指针异常为例,演示 try-catch 的使用:
public class TryCatchSingleDemo {
public static void main(String[] args) {
String str = null;
try {
// 可能抛出空指针异常的代码
System.out.println(str.length());
} catch (NullPointerException e) {
// 处理异常:打印异常信息
System.out.println("发生空指针异常:" + e.getMessage());
// 打印异常堆栈信息,便于调试
e.printStackTrace();
}
// 异常处理后,程序可以继续执行
System.out.println("程序继续运行...");
}
}
输出结果
发生空指针异常:null java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null at TryCatchSingleDemo.main(TryCatchSingleDemo.java:6) 程序继续运行...
当一段代码可能抛出多种异常时,可以使用多个 catch 块,注意异常类型要从小到大排列(子类在前,父类在后):
public class TryCatchMultiDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
// 可能抛出数组下标越界异常
System.out.println(arr[5]);
// 可能抛出空指针异常
String str = null;
System.out.println(str.length());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组下标越界:" + e.getMessage());
} catch (NullPointerException e) {
System.out.println("空指针异常:" + e.getMessage());
} catch (Exception e) {
// 父类异常,捕获所有未处理的异常
System.out.println("发生未知异常:" + e.getMessage());
}
}
}
⚠️ 注意事项:如果父类异常写在子类异常前面,会导致子类异常的 catch 块永远无法执行,编译器会报错。
finally 块用于执行必须要完成的操作,比如关闭流、释放资源等。无论 try 块是否抛出异常,finally 块都会执行。
try {
// 可能抛出异常的代码
} catch (异常类型 变量名) {
// 处理异常
} finally {
// 必须执行的代码
}
以文件读取为例,演示 finally 关闭流资源:
import java.io.FileInputStream;
import java.io.IOException;
public class FinallyDemo {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt"); // 读取文件内容
int data = fis.read();
System.out.println("读取到数据:" + data);
} catch (IOException e) {
System.out.println("文件读取异常:" + e.getMessage());
} finally {
// 无论是否异常,都要关闭流
if (fis != null) {
try {
fis.close();
System.out.println("流资源已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
输出结果(文件不存在时)
文件读取异常:test.txt (系统找不到指定的文件。)
输出结果(文件存在时)
读取到数据:97 流资源已关闭
✅ 核心结论:finally 块的唯一例外是 System.exit(0),它会直接终止 JVM,finally 块不会执行。
throws 用于在方法声明上声明该方法可能抛出的异常,将异常的处理责任交给调用者。
它适用于不想在当前方法处理异常,或者无法处理异常的场景。
public 返回值类型 方法名 (参数列表) throws 异常类型 1, 异常类型 2 {
// 方法体
}
定义一个读取文件的方法,声明抛出 IOException,由调用者处理异常:
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowsDemo {
// 声明抛出 IOException,由调用者处理
public static void readFile() throws IOException {
FileInputStream fis = new FileInputStream("test.txt");
int data = fis.read();
System.out.println("读取到数据:" + data);
fis.close();
}
public static void main(String[] args) {
try {
// 调用声明异常的方法,必须处理异常
readFile();
} catch (IOException e) {
System.out.println("处理文件读取异常:" + e.getMessage());
}
}
}
⚠️ 注意事项:
RuntimeException)可以不用 throws 声明,编译器不会强制要求。throw 用于在方法内部主动抛出一个异常对象,通常用于满足特定业务条件时触发异常。
throw new 异常类型("异常描述信息");
模拟用户登录场景,当用户名或密码错误时,主动抛出异常:
public class ThrowDemo {
public static void login(String username, String password) {
if (!"admin".equals(username)) {
// 主动抛出用户名错误异常
throw new RuntimeException("用户名错误");
}
if (!"123456".equals(password)) {
// 主动抛出密码错误异常
throw new RuntimeException("密码错误");
}
System.out.println("登录成功!");
}
public static void main(String[] args) {
try {
login("admin", "123"); // 密码错误
} catch (RuntimeException e) {
System.out.println("登录失败:" + e.getMessage());
}
}
}
输出结果
登录失败:密码错误
✅ 核心结论:throws 是方法层面的异常声明,throw 是代码层面的异常抛出,两者经常配合使用。
JDK 7 引入的 try-with-resources 语法,可以自动关闭实现了 AutoCloseable 接口的资源,无需手动在 finally 块中关闭。
常见的资源如 FileInputStream、BufferedReader、Connection 等都实现了该接口。
try (资源声明语句) {
// 使用资源的代码
} catch (异常类型 变量名) {
// 处理异常
}
对比传统 try-catch-finally 和 try-with-resources 的写法:
import java.io.FileInputStream;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// 传统写法:手动关闭资源
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
System.out.println("读取数据:" + fis.read());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources 写法:自动关闭资源
try (FileInputStream fis2 = new FileInputStream("test.txt")) {
System.out.println("读取数据:" + fis2.read());
} catch (IOException e) {
e.printStackTrace();
}
}
}
✅ 核心结论:try-with-resources 语法更简洁,代码可读性更高,是推荐的资源释放方式。
异常链是指在捕获一个异常后,抛出另一个异常并将原异常作为原因传递,这样可以保留异常的完整调用链,便于调试。
使用 initCause() 方法或异常构造方法传递原异常。
模拟业务层调用数据访问层,捕获数据访问层异常并包装为业务异常:
public class ExceptionChainDemo {
// 数据访问层方法
public static void queryData() throws SQLException {
throw new SQLException("数据库连接失败");
}
// 业务层方法
public static void processBusiness() {
try {
queryData();
} catch (SQLException e) {
// 包装为业务异常,传递原异常信息
RuntimeException businessException = new RuntimeException("业务处理失败");
businessException.initCause(e);
throw businessException;
}
}
public static void main(String[] args) {
try {
processBusiness();
} catch (RuntimeException e) {
System.out.println("异常信息:" + e.getMessage());
System.out.println("根因异常:" + e.getCause().getMessage());
// 打印完整异常堆栈
e.printStackTrace();
}
}
}
输出结果
异常信息:业务处理失败 根因异常:数据库连接失败
java.lang.RuntimeException: 业务处理失败
at ExceptionChainDemo.processBusiness(ExceptionChainDemo.java:12)
at ExceptionChainDemo.main(ExceptionChainDemo.java:19)
Caused by: java.sql.SQLException: 数据库连接失败
at ExceptionChainDemo.queryData(ExceptionChainDemo.java:4)
at ExceptionChainDemo.processBusiness(ExceptionChainDemo.java:9)
... 1 more
Java 提供的内置异常无法满足所有业务场景的需求,比如用户注册时的'用户名已存在'、'密码长度不足'等异常,需要自定义异常来描述。 自定义异常的优势:
Exception(受检异常)或 RuntimeException(非受检异常)。定义一个 UsernameExistException,表示用户名已存在异常:
// 自定义受检异常,继承 Exception
public class UsernameExistException extends Exception {
// 无参构造
public UsernameExistException() {
super();
}
// 带异常信息的构造
public UsernameExistException(String message) {
super(message);
}
// 带异常信息和根因的构造
public UsernameExistException(String message, Throwable cause) {
super(message, cause);
}
}
定义一个 PasswordLengthException,表示密码长度不足异常:
// 自定义非受检异常,继承 RuntimeException
public class PasswordLengthException extends RuntimeException {
public PasswordLengthException() {
super();
}
public PasswordLengthException(String message) {
super(message);
}
public PasswordLengthException(String message, Throwable cause) {
super(message, cause);
}
}
模拟用户注册功能,使用自定义异常处理业务逻辑:
import java.util.HashSet;
import java.util.Set;
public class CustomExceptionDemo {
// 模拟已存在的用户名
private static final Set<String> EXIST_USERNAMES = new HashSet<>();
static {
EXIST_USERNAMES.add("admin");
EXIST_USERNAMES.add("user1");
}
// 注册方法
public static void register(String username, String password) throws UsernameExistException {
// 检查用户名是否存在
if (EXIST_USERNAMES.contains(username)) {
throw new UsernameExistException("用户名 [" + username + "] 已存在");
}
// 检查密码长度
if (password.length() < 6) {
throw new PasswordLengthException("密码长度不能小于 6 位");
}
// 注册成功
EXIST_USERNAMES.add(username);
System.out.println("用户 [" + username + "] 注册成功!");
}
public static void main(String[] args) {
try {
register("admin", "123456"); // 用户名已存在
} catch (UsernameExistException e) {
System.out.println("注册失败:" + e.getMessage());
} catch (PasswordLengthException e) {
System.out.println("注册失败:" + e.getMessage());
}
try {
register("user2", "123"); // 密码长度不足
} catch (UsernameExistException e) {
System.out.println("注册失败:" + e.getMessage());
} catch (PasswordLengthException e) {
System.out.println("注册失败:" + e.getMessage());
}
}
}
输出结果
注册失败:用户名 [admin] 已存在
注册失败:密码长度不能小于 6 位
⚠️ 注意事项:自定义异常尽量继承 RuntimeException,避免强制调用者处理,减少代码冗余。
Exception:避免捕获所有异常,导致无法定位具体问题。catch 块,至少要打印异常信息。try-with-resources 或 finally 块确保资源被正确释放。❌ 误区 1:空 catch 块,忽略异常
try {
// 可能抛出异常的代码
} catch (NullPointerException e) {
// 空的 catch 块,异常被忽略,无法定位问题
}
✅ 正确做法:打印异常信息
try {
// 可能抛出异常的代码
} catch (NullPointerException e) {
e.printStackTrace();
}
❌ 误区 2:捕获 Exception,处理所有异常
try {
// 可能抛出多种异常的代码
} catch (Exception e) {
System.out.println("发生异常");
}
✅ 正确做法:捕获具体异常
try {
// 可能抛出多种异常的代码
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组下标越界");
}
❌ 误区 3:过度使用异常,替代正常逻辑判断
// 不好的写法:用异常处理替代空指针判断
public static int getLength(String str) {
try {
return str.length();
} catch (NullPointerException e) {
return 0;
}
}
✅ 正确做法:提前进行逻辑判断
public static int getLength(String str) {
if (str == null) {
return 0;
}
return str.length();
}
在实际项目中,需要一个统一的异常处理框架,将系统异常和业务异常进行统一封装,返回友好的错误信息。 需求如下:
/**
* 全局异常枚举
*/
public enum ErrorCode {
SUCCESS(200, "操作成功"),
USERNAME_EXIST(1001, "用户名已存在"),
PASSWORD_LENGTH_ERROR(1002, "密码长度不能小于 6 位"),
SYSTEM_ERROR(500, "系统内部异常");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
/**
* 统一返回结果类
*/
public class Result<T> {
// 错误码
private int code;
// 错误信息
private String message;
// 数据
private T data;
// 私有构造
private Result() {}
// 成功响应
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.code = ErrorCode.SUCCESS.getCode();
result.message = ErrorCode.SUCCESS.getMessage();
result.data = data;
return result;
}
// 失败响应
public static <T> Result<T> fail(ErrorCode errorCode) {
Result<T> result = new Result<>();
result.code = errorCode.getCode();
result.message = errorCode.getMessage();
return result;
}
// 失败响应,自定义错误信息
public static <T> Result<T> fail(int code, String message) {
Result<T> result = new Result<>();
result.code = code;
result.message = message;
return result;
}
// getter 和 setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "Result{" + "code=" + code + ", message='" + message + "', data=" + data + '}';
}
}
/**
* 全局业务异常
*/
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
/**
* 全局异常处理器
*/
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
public static Result<?> handleBusinessException(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理系统异常
*/
public static Result<?> handleSystemException(Exception e) {
// 打印异常堆栈,便于调试
e.printStackTrace();
return Result.fail(ErrorCode.SYSTEM_ERROR);
}
/**
* 统一异常处理入口
*/
public static Result<?> handleException(Throwable e) {
if (e instanceof BusinessException) {
return handleBusinessException((BusinessException) e);
} else {
return handleSystemException((Exception) e);
}
}
}
public class GlobalExceptionDemo {
public static void register(String username, String password) {
if ("admin".equals(username)) {
// 抛出自定义业务异常
throw new BusinessException(ErrorCode.USERNAME_EXIST);
}
if (password.length() < 6) {
throw new BusinessException(ErrorCode.PASSWORD_LENGTH_ERROR);
}
}
public static void main(String[] args) {
// 测试用户名已存在异常
try {
register("admin", "123456");
} catch (Throwable e) {
Result<?> result = GlobalExceptionHandler.handleException(e);
System.out.println(result);
}
// 测试密码长度不足异常
try {
register("user2", "123");
} catch (Throwable e) {
Result<?> result = GlobalExceptionHandler.handleException(e);
System.out.println(result);
}
// 测试系统异常
try {
String str = null;
System.out.println(str.length());
} catch (Throwable e) {
Result<?> result = GlobalExceptionHandler.handleException(e);
System.out.println(result);
}
}
}
输出结果
Result{code=1001, message='用户名已存在', data=null}
Result{code=1002, message='密码长度不能小于 6 位', data=null}
java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null at GlobalExceptionDemo.main(GlobalExceptionDemo.java:28)
Result{code=500, message='系统内部异常', data=null}
✅ 这个统一异常处理框架是实际项目中的标准做法,它的优势在于:
Error、Checked Exception 和 Unchecked Exception,其中 Error 无法处理,Unchecked Exception 建议通过代码规范避免。try-catch-finally、throws、throw,try-with-resources 是推荐的资源释放方式。RuntimeException。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online