JAVA 异常处理:从原理到实战最佳实践
JAVA 异常处理:从原理到实战最佳实践
1.1 本章学习目标与重点
💡 掌握异常的分类与核心概念,理解异常处理的设计思想。
💡 熟练运用 try-catch-finally、throws、throw 处理异常。
💡 掌握自定义异常的编写与使用场景,规范异常处理流程。
⚠️ 本章重点是 异常处理的最佳实践 和 避免常见误区,这是提升代码健壮性的核心技能。
1.2 异常的核心概念与分类
1.2.1 什么是异常
💡 异常是指程序运行过程中出现的非正常情况,它会中断程序的正常执行流程。
比如文件找不到、数组下标越界、空指针访问等,这些情况都会触发异常。
Java 中所有异常都是 Throwable 类的子类,异常处理的本质是捕获并处理这些非正常情况,保证程序可以继续运行或优雅退出。
1.2.2 异常的分类
Java 中的异常体系分为三大类,它们的父类都是 Throwable:
- 是 JVM 内部的严重错误,属于不可恢复的异常,程序无法处理。
- 常见的有
OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出)。 - 示例:递归调用没有终止条件,会导致栈溢出
- Checked Exception(受检异常)
- 也称编译时异常,编译器会强制要求程序处理这类异常。
- 常见的有
IOException(IO 异常)、SQLException(数据库异常)。 - 处理方式:要么用
try-catch捕获,要么用throws声明抛出。
- Unchecked Exception(非受检异常)
- 也称运行时异常,继承自
RuntimeException,编译器不强制处理。 - 常见的有
NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组下标越界)、ClassCastException(类型转换异常)。 - 这类异常通常是程序逻辑错误导致的,建议通过代码规范来避免,而不是捕获处理。
- 也称运行时异常,继承自
Error(错误)
publicclassErrorDemo{publicstaticvoidrecursion(){recursion();// 无限递归}publicstaticvoidmain(String[] args){recursion();// 抛出 StackOverflowError}}✅ 核心结论:异常体系的继承关系为 Throwable → Error / Exception → RuntimeException。
1.2.3 常见异常及其产生原因
| 异常类型 | 产生原因 |
|---|---|
NullPointerException | 调用了 null 对象的方法或访问了 null 对象的属性 |
ArrayIndexOutOfBoundsException | 数组下标超出了数组的长度范围 |
ClassCastException | 强制类型转换失败,比如将 String 转为 Integer |
ArithmeticException | 算术运算错误,比如整数除以 0 |
IOException | 文件读写失败,比如文件不存在或权限不足 |
1.3 异常处理的核心语法
1.3.1 try-catch:捕获并处理异常
💡 try-catch 是最基础的异常处理结构,try 块包裹可能抛出异常的代码,catch 块捕获并处理对应的异常。
基本语法
try{// 可能抛出异常的代码}catch(异常类型1 变量名1){// 处理异常类型1的逻辑}catch(异常类型2 变量名2){// 处理异常类型2的逻辑}代码实操:捕获单个异常
以空指针异常为例,演示 try-catch 的使用:
publicclassTryCatchSingleDemo{publicstaticvoidmain(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 块,注意异常类型要从小到大排列(子类在前,父类在后):
publicclassTryCatchMultiDemo{publicstaticvoidmain(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 块永远无法执行,编译器会报错。
1.3.2 finally:无论是否异常都会执行
💡 finally 块用于执行必须要完成的操作,比如关闭流、释放资源等。无论 try 块是否抛出异常,finally 块都会执行。
基本语法
try{// 可能抛出异常的代码}catch(异常类型 变量名){// 处理异常}finally{// 必须执行的代码}代码实操:finally 释放资源
以文件读取为例,演示 finally 关闭流资源:
importjava.io.FileInputStream;importjava.io.IOException;publicclassFinallyDemo{publicstaticvoidmain(String[] args){FileInputStream fis =null;try{ fis =newFileInputStream("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 块不会执行。
1.3.3 throws:声明抛出异常
💡 throws 用于在方法声明上声明该方法可能抛出的异常,将异常的处理责任交给调用者。
它适用于不想在当前方法处理异常,或者无法处理异常的场景。
基本语法
public 返回值类型 方法名(参数列表)throws 异常类型1, 异常类型2{// 方法体}代码实操:throws 声明异常
定义一个读取文件的方法,声明抛出 IOException,由调用者处理异常:
importjava.io.FileInputStream;importjava.io.IOException;publicclassThrowsDemo{// 声明抛出 IOException,由调用者处理publicstaticvoidreadFile()throwsIOException{FileInputStream fis =newFileInputStream("test.txt");int data = fis.read();System.out.println("读取到数据:"+ data); fis.close();}publicstaticvoidmain(String[] args){try{// 调用声明异常的方法,必须处理异常readFile();}catch(IOException e){System.out.println("处理文件读取异常:"+ e.getMessage());}}}⚠️ 注意事项:
- 非受检异常(
RuntimeException)可以不用throws声明,编译器不会强制要求。 - 子类重写父类方法时,抛出的异常不能超过父类方法声明的异常范围(子类异常 ≤ 父类异常)。
1.3.4 throw:主动抛出异常
💡 throw 用于在方法内部主动抛出一个异常对象,通常用于满足特定业务条件时触发异常。
基本语法
thrownew 异常类型("异常描述信息");代码实操:throw 主动抛异常
模拟用户登录场景,当用户名或密码错误时,主动抛出异常:
publicclassThrowDemo{publicstaticvoidlogin(String username,String password){if(!"admin".equals(username)){// 主动抛出用户名错误异常thrownewRuntimeException("用户名错误");}if(!"123456".equals(password)){// 主动抛出密码错误异常thrownewRuntimeException("密码错误");}System.out.println("登录成功!");}publicstaticvoidmain(String[] args){try{login("admin","123");// 密码错误}catch(RuntimeException e){System.out.println("登录失败:"+ e.getMessage());}}}输出结果
登录失败:密码错误 ✅ 核心结论:throws 是方法层面的异常声明,throw 是代码层面的异常抛出,两者经常配合使用。
1.4 异常处理的进阶特性
1.4.1 try-with-resources:自动释放资源
💡 JDK 7 引入的 try-with-resources 语法,可以自动关闭实现了 AutoCloseable 接口的资源,无需手动在 finally 块中关闭。
常见的资源如 FileInputStream、BufferedReader、Connection 等都实现了该接口。
基本语法
try(资源声明语句){// 使用资源的代码}catch(异常类型 变量名){// 处理异常}代码实操:自动关闭文件流
对比传统 try-catch-finally 和 try-with-resources 的写法:
importjava.io.FileInputStream;importjava.io.IOException;publicclassTryWithResourcesDemo{publicstaticvoidmain(String[] args){// 传统写法:手动关闭资源FileInputStream fis =null;try{ fis =newFileInputStream("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 =newFileInputStream("test.txt")){System.out.println("读取数据:"+ fis2.read());}catch(IOException e){ e.printStackTrace();}}}✅ 核心结论:try-with-resources 语法更简洁,代码可读性更高,是推荐的资源释放方式。
1.4.2 异常链:传递异常信息
💡 异常链是指在捕获一个异常后,抛出另一个异常并将原异常作为原因传递,这样可以保留异常的完整调用链,便于调试。
使用 initCause() 方法或异常构造方法传递原异常。
代码实操:异常链的使用
模拟业务层调用数据访问层,捕获数据访问层异常并包装为业务异常:
publicclassExceptionChainDemo{// 数据访问层方法publicstaticvoidqueryData()throwsSQLException{thrownewSQLException("数据库连接失败");}// 业务层方法publicstaticvoidprocessBusiness(){try{queryData();}catch(SQLException e){// 包装为业务异常,传递原异常信息RuntimeException businessException =newRuntimeException("业务处理失败"); businessException.initCause(e);throw businessException;}}publicstaticvoidmain(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 1.5 自定义异常
1.5.1 为什么需要自定义异常
💡 Java 提供的内置异常无法满足所有业务场景的需求,比如用户注册时的“用户名已存在”、“密码长度不足”等异常,需要自定义异常来描述。
自定义异常的优势:
- 异常信息更贴合业务,便于开发者定位问题。
- 可以区分系统异常和业务异常,便于统一处理。
1.5.2 自定义异常的编写步骤
- 定义一个类,继承
Exception(受检异常)或RuntimeException(非受检异常)。 - 提供无参构造方法和带异常信息的构造方法。
- (可选)提供带异常信息和根因异常的构造方法。
1.5.3 代码实操1:自定义受检异常
定义一个 UsernameExistException,表示用户名已存在异常:
// 自定义受检异常,继承 ExceptionpublicclassUsernameExistExceptionextendsException{// 无参构造publicUsernameExistException(){super();}// 带异常信息的构造publicUsernameExistException(String message){super(message);}// 带异常信息和根因的构造publicUsernameExistException(String message,Throwable cause){super(message, cause);}}1.5.4 代码实操2:自定义非受检异常
定义一个 PasswordLengthException,表示密码长度不足异常:
// 自定义非受检异常,继承 RuntimeExceptionpublicclassPasswordLengthExceptionextendsRuntimeException{publicPasswordLengthException(){super();}publicPasswordLengthException(String message){super(message);}publicPasswordLengthException(String message,Throwable cause){super(message, cause);}}1.5.5 自定义异常的使用场景
模拟用户注册功能,使用自定义异常处理业务逻辑:
importjava.util.HashSet;importjava.util.Set;publicclassCustomExceptionDemo{// 模拟已存在的用户名privatestaticfinalSet<String>EXIST_USERNAMES=newHashSet<>();static{EXIST_USERNAMES.add("admin");EXIST_USERNAMES.add("user1");}// 注册方法publicstaticvoidregister(String username,String password)throwsUsernameExistException{// 检查用户名是否存在if(EXIST_USERNAMES.contains(username)){thrownewUsernameExistException("用户名["+ username +"]已存在");}// 检查密码长度if(password.length()<6){thrownewPasswordLengthException("密码长度不能小于6位");}// 注册成功EXIST_USERNAMES.add(username);System.out.println("用户["+ username +"]注册成功!");}publicstaticvoidmain(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,避免强制调用者处理,减少代码冗余。
1.6 异常处理的最佳实践
1.6.1 核心原则
- 只捕获能处理的异常:对于无法处理的异常,应该向上抛出,让上层调用者处理。
- 捕获具体异常,而非笼统的
Exception:避免捕获所有异常,导致无法定位具体问题。 - 不要忽略异常:禁止空的
catch块,至少要打印异常信息。 - 释放资源:使用
try-with-resources或finally块确保资源被正确释放。 - 提供有意义的异常信息:异常信息要明确,便于开发者定位问题,比如“用户名[admin]已存在”。
1.6.2 常见误区
❌ 误区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:过度使用异常,替代正常逻辑判断
// 不好的写法:用异常处理替代空指针判断publicstaticintgetLength(String str){try{return str.length();}catch(NullPointerException e){return0;}}✅ 正确做法:提前进行逻辑判断
publicstaticintgetLength(String str){if(str ==null){return0;}return str.length();}1.7 实战案例:统一异常处理框架
1.7.1 需求分析
💡 在实际项目中,需要一个统一的异常处理框架,将系统异常和业务异常进行统一封装,返回友好的错误信息。
需求如下:
- 定义全局异常枚举,包含错误码和错误信息。
- 定义统一的返回结果类,封装接口返回数据。
- 定义全局异常处理器,捕获所有异常并统一处理。
1.7.2 代码实现
步骤1:定义异常枚举
/** * 全局异常枚举 */publicenumErrorCode{SUCCESS(200,"操作成功"),USERNAME_EXIST(1001,"用户名已存在"),PASSWORD_LENGTH_ERROR(1002,"密码长度不能小于6位"),SYSTEM_ERROR(500,"系统内部异常");privatefinalint code;privatefinalString message;ErrorCode(int code,String message){this.code = code;this.message = message;}publicintgetCode(){return code;}publicStringgetMessage(){return message;}}步骤2:定义统一返回结果类
/** * 统一返回结果类 */publicclassResult<T>{// 错误码privateint code;// 错误信息privateString message;// 数据privateT data;// 私有构造privateResult(){}// 成功响应publicstatic<T>Result<T>success(T data){Result<T> result =newResult<>(); result.code =ErrorCode.SUCCESS.getCode(); result.message =ErrorCode.SUCCESS.getMessage(); result.data = data;return result;}// 失败响应publicstatic<T>Result<T>fail(ErrorCode errorCode){Result<T> result =newResult<>(); result.code = errorCode.getCode(); result.message = errorCode.getMessage();return result;}// 失败响应,自定义错误信息publicstatic<T>Result<T>fail(int code,String message){Result<T> result =newResult<>(); result.code = code; result.message = message;return result;}// getter和setterpublicintgetCode(){return code;}publicvoidsetCode(int code){this.code = code;}publicStringgetMessage(){return message;}publicvoidsetMessage(String message){this.message = message;}publicTgetData(){return data;}publicvoidsetData(T data){this.data = data;}@OverridepublicStringtoString(){return"Result{"+"code="+ code +",+ message +'\''+", data="+ data +'}';}}步骤3:定义全局业务异常
/** * 全局业务异常 */publicclassBusinessExceptionextendsRuntimeException{privatefinalint code;publicBusinessException(ErrorCode errorCode){super(errorCode.getMessage());this.code = errorCode.getCode();}publicBusinessException(int code,String message){super(message);this.code = code;}publicintgetCode(){return code;}}步骤4:定义全局异常处理器
/** * 全局异常处理器 */publicclassGlobalExceptionHandler{/** * 处理业务异常 */publicstaticResult<?>handleBusinessException(BusinessException e){returnResult.fail(e.getCode(), e.getMessage());}/** * 处理系统异常 */publicstaticResult<?>handleSystemException(Exception e){// 打印异常堆栈,便于调试 e.printStackTrace();returnResult.fail(ErrorCode.SYSTEM_ERROR);}/** * 统一异常处理入口 */publicstaticResult<?>handleException(Throwable e){if(e instanceofBusinessException){returnhandleBusinessException((BusinessException) e);}else{returnhandleSystemException((Exception) e);}}}步骤5:测试统一异常处理框架
publicclassGlobalExceptionDemo{publicstaticvoidregister(String username,String password){if("admin".equals(username)){// 抛出自定义业务异常thrownewBusinessException(ErrorCode.USERNAME_EXIST);}if(password.length()<6){thrownewBusinessException(ErrorCode.PASSWORD_LENGTH_ERROR);}}publicstaticvoidmain(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} 1.7.3 案例总结
✅ 这个统一异常处理框架是实际项目中的标准做法,它的优势在于:
- 统一了异常处理逻辑,减少重复代码。
- 区分了业务异常和系统异常,便于问题定位。
- 返回友好的错误信息,提升用户体验。
1.8 本章总结
- 异常分为
Error、Checked Exception和Unchecked Exception,其中Error无法处理,Unchecked Exception建议通过代码规范避免。 - 异常处理的核心语法包括
try-catch-finally、throws、throw,try-with-resources是推荐的资源释放方式。 - 自定义异常要贴合业务场景,优先继承
RuntimeException。 - 异常处理的最佳实践是:捕获具体异常、不忽略异常、释放资源、提供有意义的异常信息。
- 实际项目中要搭建统一异常处理框架,实现异常的统一封装和处理。