Java 常见Exception全面解析:出现场景、错误排查与代码修正实战

文章目录

课程导言
适用对象
本课程适合已经掌握Java基础语法,初步了解异常处理概念,但希望系统掌握常见异常排查与修复能力的开发者。无论你是刚入行的新人,还是有一定经验的开发者,这门课程都将帮助你建立系统的异常排查思维,提升代码质量。
学习目标
通过两个课时的系统学习,你将能够:
- 识别 Java中10+种最常见的异常及其产生场景
- 分析 异常堆栈信息,快速定位问题根源
- 掌握 针对不同异常的系统性排查方法
- 运用 最佳实践修复代码,预防同类问题再次发生
- 建立 异常处理的正确思维模式
课程安排
- 第一课时(约60分钟):异常基础概念 + 运行时异常深度剖析(NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、ArithmeticException)
- 第二课时(约60分钟):受检异常深度剖析 + 复杂异常排查 + 综合实战演练
教学方式
每个异常都遵循“现象描述 → 出现场景 → 堆栈分析 → 排查方法 → 代码修正 → 预防措施”的六步教学法,确保理论与实践紧密结合。
第一部分:Java异常体系回顾(约10分钟)
1.1 异常是什么?
在深入具体异常之前,我们先理解异常的本质。异常(Exception) 是程序运行过程中出现的打断正常执行流程的事件。它本质上是一个对象,封装了错误类型、错误描述、方法调用堆栈以及可能的底层原因。
1.2 Java异常体系结构
java.lang.Object | java.lang.Throwable | --------------------- | | java.lang.Error java.lang.Exception | ------------------------- | | RuntimeException 其他 Exception (运行时异常) (受检异常) - Error:JVM级别的严重错误,如
OutOfMemoryError、StackOverflowError,程序通常无法处理。 - Exception:程序可处理的异常,分为两类:
- 受检异常(Checked Exception):编译时必须处理(捕获或声明抛出),如
IOException、SQLException。 - 运行时异常(RuntimeException):编译时不强制处理,通常由程序逻辑错误导致,如
NullPointerException、ArrayIndexOutOfBoundsException。
- 受检异常(Checked Exception):编译时必须处理(捕获或声明抛出),如
1.3 异常信息解读
一个典型的异常堆栈包含以下要素:
Exception java.lang.IllegalArgumentException: item quantity must be a number at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:45) // … Caused by java.lang.NumberFormatException: For input string: " 3" at NumberFormatException.forInputString (NumberFormatException.java:67) at Integer.parseInt (Integer.java:647) ... - 异常类型:
IllegalArgumentException - 异常消息:
item quantity must be a number - 堆栈轨迹:从main开始到异常发生处的调用链
- Caused by:底层根本原因,通常是排查的关键入口
排查技巧:遇到复杂异常时,不要只看第一行,要顺着堆栈往下找,尤其是“Caused by”部分,那里往往藏着真正的原因。
第二课时(上):运行时异常深度剖析(约30分钟)
运行时异常(RuntimeException)是Java程序中最常见的异常类型,它们通常由代码逻辑错误引起。下面我们将逐个剖析最常见的运行时异常。
2.1 NullPointerException(空指针异常)
现象描述
当应用程序试图在需要对象的地方使用null引用时,抛出此异常。这是Java中最著名的异常,占据了异常总数的很大比例。
出现场景
场景一:直接调用null对象的方法或属性
String text =null;int length = text.length();// 抛出NullPointerException场景二:自动拆箱时包装类型为null
Boolean willVote =null;if(willVote){// 自动拆箱时抛出NullPointerExceptionSystem.out.println("可以投票");}场景三:方法参数或返回值未做空检查
voidparseDocument(Document doc){ doc.getElements();// 如果传入的doc为null,抛出异常}StringlookupElement(Document doc){Element element = doc.findElement("span");return element.getValue();// 如果element为null,抛出异常}场景四:数组元素未初始化
Person[] people =newPerson[5]; people[0].getName();// 数组元素默认为null,抛出异常堆栈分析示例
Exception in thread "main" java.lang.NullPointerException at com.example.UserService.getUserAge(UserService.java:25) at com.example.UserController.main(UserController.java:12) 从堆栈可以看出,UserService.java的第25行调用了某个null对象的方法。
排查方法流程图
方法参数
方法返回值
未初始化变量
数组元素
发现NullPointerException
定位堆栈中第一个出现自己代码的行号
检查该行代码有哪些对象可能为null
对象来源是什么?
检查调用方是否传入null
检查被调用方法是否可能返回null
检查变量是否已正确初始化
检查数组元素是否已赋值
修复调用方或添加空值校验
代码修正与预防
修正方案一:参数校验
// 错误代码voidparseDocument(Document doc){ doc.getElements();}// 修正代码voidparseDocument(@NonNullDocument doc){if(doc ==null){thrownewIllegalArgumentException("doc cannot be null");} doc.getElements();}修正方案二:使用守卫语句
// 错误代码StringlookupElement(Document doc){Element element = doc.findElement("span");return element.getValue();}// 修正代码@NullableStringlookupElement(Document doc){Element element = doc.findElement("span");if(element ==null){returnnull;// 或者返回默认值}return element.getValue();}修正方案三:使用Optional(Java 8+)
publicOptional<String>lookupElement(Document doc){returnOptional.ofNullable(doc.findElement("span")).map(Element::getValue);}修正方案四:数组元素初始化
Person[] people =newPerson[5];for(int i =0; i < people.length; i++){ people[i]=newPerson();// 确保每个元素都被初始化}预防措施 :
- 明确空值来源:是无效值(上游问题)还是有效值(可接受null)
- 使用@NonNull和@Nullable注解,让IDE帮助检查
- 遵循"尽早失败"原则:在方法入口处就进行参数校验
- 谨慎处理返回值:明确方法是否可能返回null,并在文档中说明
2.2 ArrayIndexOutOfBoundsException(数组下标越界异常)
现象描述
当试图使用非法索引访问数组元素时抛出,非法索引包括负数、0到数组长度减1范围外的值。
出现场景
场景一:索引超出数组长度
int[] numbers ={1,2,3};int value = numbers[3];// 索引3超出范围(有效索引0-2)场景二:循环条件错误
int[] scores ={85,90,78,92};for(int i =0; i <= scores.length; i++){// 应该是 i < scores.lengthSystem.out.println(scores[i]);// 最后一次循环i=4,越界}场景三:索引为负数
int[] data =newint[10];int index =-1; data[index]=100;// 负索引越界堆栈分析示例
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 at com.example.ArrayDemo.processArray(ArrayDemo.java:15) at com.example.ArrayDemo.main(ArrayDemo.java:8) 异常信息直接告诉我们:试图访问索引5,但数组长度只有5(有效索引0-4)。
排查方法
直接常量
循环变量
计算结果
发现ArrayIndexOutOfBoundsException
定位堆栈中的代码行
检查该行代码的数组访问表达式
索引值是如何计算的?
检查常量是否在合法范围内
检查循环条件边界
检查计算逻辑是否有误
修正索引值
代码修正与预防
修正方案一:修正循环边界
// 错误代码for(int i =0; i <= scores.length; i++){System.out.println(scores[i]);}// 修正代码for(int i =0; i < scores.length; i++){System.out.println(scores[i]);}// 更好的方式:使用增强for循环for(int score : scores){System.out.println(score);}修正方案二:访问前检查索引
publicintgetElement(int[] array,int index){if(array ==null){thrownewIllegalArgumentException("array cannot be null");}if(index <0|| index >= array.length){thrownewIndexOutOfBoundsException("Index "+ index +" out of bounds for length "+ array.length);}return array[index];}预防措施:
- 优先使用增强for循环处理数组遍历
- 使用
Arrays工具类的方法进行数组操作 - 动态计算索引时,添加边界检查
- **考虑使用
ArrayList**等集合类,它们提供了更安全的get()方法(也会抛出越界异常,但信息更明确)
2.3 ClassCastException(类型转换异常)
现象描述
当试图将一个对象强制转换为它不是实例的子类时抛出。这是使用继承和多态时的常见问题。
出现场景
场景一:将父类对象强制转换为子类类型
Object obj =newObject();Integer num =(Integer) obj;// Object不能转换为Integer场景二:集合中元素类型不一致
List list =newArrayList(); list.add("Hello"); list.add(123);// 混合类型String first =(String) list.get(0);// 正常String second =(String) list.get(1);// 抛出ClassCastException,123不能转String场景三:不正确的向下转型
Animal animal =newDog();Cat cat =(Cat) animal;// Dog不能转换为Cat堆栈分析示例
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String at com.example.GenericDemo.processList(GenericDemo.java:22) at com.example.GenericDemo.main(GenericDemo.java:15) 异常信息明确告诉我们:试图将Integer转换为String,类型不兼容。
排查方法
集合
方法返回值
从外部系统获取
发现ClassCastException
定位堆栈中的转换代码行
检查被转换对象的实际类型
对象来源是什么?
检查集合中元素类型是否一致
检查返回类型的实际实现
检查序列化/反序列化过程
使用泛型或instanceof检查
代码修正与预防
修正方案一:使用泛型
// 错误代码List list =newArrayList(); list.add("Hello");String s =(String) list.get(0);// 修正代码:使用泛型List<String> list =newArrayList<>(); list.add("Hello");String s = list.get(0);// 无需强制转换修正方案二:使用instanceof检查
Object obj =getSomeObject();if(obj instanceofString){String str =(String) obj;// 安全转换// 处理字符串}elseif(obj instanceofInteger){Integer num =(Integer) obj;// 安全转换// 处理整数}修正方案三:Java 16+的Pattern Matching for instanceof
Object obj =getSomeObject();if(obj instanceofString str){// 这里可以直接使用str变量System.out.println(str.length());}elseif(obj instanceofInteger num){System.out.println(num +10);}else{// 处理其他情况}预防措施:
- 始终使用泛型确保集合类型安全
- 在向下转型前使用
instanceof检查 - 遵循里氏替换原则,避免不必要的向下转型
- 考虑使用多态,而不是频繁的类型转换
2.4 ArithmeticException(算术异常)
现象描述
当发生异常的算术条件时抛出,最常见的是整数除零。
出现场景
场景一:整数除零
int result =10/0;// 抛出ArithmeticException场景二:取模运算除零
int remainder =10%0;// 抛出ArithmeticException注意:浮点数除零不会抛出异常,会返回Infinity或NaN
double result =10.0/0.0;// 返回 Infinity,不会抛出异常堆栈分析示例
Exception in thread "main" java.lang.ArithmeticException: / by zero at com.example.Calculator.divide(Calculator.java:10) at com.example.Calculator.main(Calculator.java:5) 异常信息直接告诉我们问题:除零。
排查方法
直接常量
变量
方法返回值
发现ArithmeticException
定位堆栈中的除法/取模代码
检查分母/右操作数的值
分母来源是什么?
修改常量为非零值
检查变量赋值逻辑
检查返回值的范围
添加除零检查
代码修正与预防
修正方案一:检查除数
publicintdivide(int a,int b){if(b ==0){thrownewIllegalArgumentException("除数不能为0");}return a / b;}修正方案二:使用Optional处理可能为0的情况
publicOptional<Integer>safeDivide(int a,int b){if(b ==0){returnOptional.empty();}returnOptional.of(a / b);}修正方案三:使用浮点数运算(如果业务允许)
double result =10.0/0.0;// 返回 Infinity,不会抛出异常if(Double.isInfinite(result)){// 处理无穷大情况}预防措施:
- 在进行除法或取模前,始终检查除数是否为0
- **考虑使用
BigDecimal**进行精确计算,它提供了更好的异常处理 - 从用户输入获取除数时,必须进行验证
第二课时(中):运行时异常(续)与常见受检异常(约20分钟)
2.5 NumberFormatException(数字格式异常)
现象描述
当尝试将字符串转换为数字类型,但字符串格式不合法时抛出。
出现场景
场景一:字符串包含非数字字符
int num =Integer.parseInt("123abc");// 抛出NumberFormatException场景二:字符串包含空格或特殊符号
int num =Integer.parseInt(" 123 ");// 抛出NumberFormatException,空格未处理场景三:数字超出类型范围
int num =Integer.parseInt("2147483648");// 超出int最大值,抛出异常场景四:空字符串或null
int num =Integer.parseInt("");// 抛出NumberFormatExceptionInteger.parseInt(null);// 抛出NullPointerException,注意这里是NPE堆栈分析示例
Exception in thread "main" java.lang.NumberFormatException: For input string: " 123 " at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:654) at java.base/java.lang.Integer.parseInt(Integer.java:786) at com.example.UserInput.processAge(UserInput.java:12) 排查方法
渲染错误: Mermaid 渲染失败: Parse error on line 6: ...-->|包含空格| F[考虑使用trim()去除前后空格] D -->| -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
代码修正与预防
修正方案一:数据清洗
// 错误代码String input =" 123 ";int value =Integer.parseInt(input);// 抛出异常// 修正代码String input =" 123 "; input = input.trim();// 去除前后空格if(!input.isEmpty()){try{int value =Integer.parseInt(input);}catch(NumberFormatException e){// 处理异常}}修正方案二:使用正则表达式预验证
publicintparsePostalCode(String input){// 预验证:必须是5位数字if(input ==null||!input.matches("\\d{5}")){thrownewIllegalArgumentException("邮政编码必须是5位数字");}returnInteger.parseInt(input);// 此时已保证安全}修正方案三:使用Apache Commons Lang的NumberUtils
importorg.apache.commons.lang3.math.NumberUtils;String input ="123";int value =NumberUtils.toInt(input,0);// 失败时返回默认值0,不抛出异常修正方案四:Java 8+的Optional + 异常处理
publicOptional<Integer>tryParseInt(String input){try{returnOptional.of(Integer.parseInt(input.trim()));}catch(NumberFormatException e){returnOptional.empty();}}预防措施 :
- 始终对输入进行清洗(trim、去除非数字字符)
- 解析前验证格式,特别是来自外部系统的数据
- 考虑使用专门的验证框架如Hibernate Validator
- 使用
try-catch包围解析代码,优雅处理异常
2.6 IllegalArgumentException(非法参数异常)
现象描述
当向方法传递了不合法或不适当的参数时抛出。这通常表示调用者的责任。
出现场景
场景一:参数值超出允许范围
publicvoidsetAge(int age){if(age <0|| age >150){thrownewIllegalArgumentException("年龄必须在0-150之间");}this.age = age;}场景二:参数格式错误
publicvoidsetEmail(String email){if(email ==null||!email.contains("@")){thrownewIllegalArgumentException("邮箱格式不正确");}this.email = email;}场景三:参数为null但方法不允许
publicvoidprocessData(@NonNullData data){if(data ==null){thrownewIllegalArgumentException("data cannot be null");}// 处理数据}排查方法
- 查看异常消息,通常会说明参数需要满足什么条件
- 检查调用代码,确认传入的参数值
- 验证参数来源,判断是输入错误还是上游数据问题
代码修正
// 在方法开头进行参数校验publicvoidregisterUser(String username,String email,int age){// 参数校验集中处理if(username ==null|| username.trim().isEmpty()){thrownewIllegalArgumentException("用户名不能为空");}if(email ==null||!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")){thrownewIllegalArgumentException("邮箱格式不正确");}if(age <0|| age >150){thrownewIllegalArgumentException("年龄无效");}// 业务逻辑}2.7 IllegalStateException(非法状态异常)
现象描述
当方法在非法或不适当的时间被调用时抛出。通常表示被调用者的状态不适合执行请求的操作。
出现场景
场景一:对象未正确初始化
publicclassConnectionPool{privateboolean initialized =false;publicvoidconnect(){if(!initialized){thrownewIllegalStateException("连接池未初始化");}// 建立连接}}场景二:迭代器越界
List<String> list =Arrays.asList("A","B");Iterator<String> it = list.iterator(); it.next();// A it.next();// B it.next();// 抛出NoSuchElementException,但有时会被包装为IllegalStateException排查方法
- 阅读异常消息,了解当前对象应该处于什么状态
- 检查对象初始化或配置代码,确保在调用前已正确设置
- 检查操作顺序,确认是否按正确步骤调用
代码修正
publicclassFileProcessor{privateboolean opened =false;publicvoidopen(){// 打开文件 opened =true;}publicvoidreadData(){if(!opened){thrownewIllegalStateException("必须先调用open()方法打开文件");}// 读取数据}}2.8 IOException(输入输出异常)
现象描述
当输入输出操作失败或中断时抛出。这是最典型的受检异常,处理文件、网络、流操作时经常遇到。
出现场景
场景一:文件不存在(FileNotFoundException)
FileReader fr =newFileReader("nonexistent.txt");// 抛出FileNotFoundException场景二:读取流时连接断开
InputStream in = socket.getInputStream();int data = in.read();// 如果连接已关闭,可能抛出IOException场景三:写入磁盘空间不足
FileOutputStream fos =newFileOutputStream("largefile.bin");byte[] data =newbyte[1024]; fos.write(data);// 如果磁盘空间不足,抛出IOException堆栈分析示例
java.io.FileNotFoundException: nonexistent.txt (系统找不到指定的文件) at java.base/java.io.FileInputStream.open0(Native Method) at java.base/java.io.FileInputStream.open(FileInputStream.java:219) at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157) at com.example.FileReaderDemo.main(FileReaderDemo.java:8) 排查方法
FileNotFoundException
EOFException
SocketException
其他IO错误
发现IOException
查看异常消息中的具体原因
原因类型?
检查文件路径和存在性
检查是否提前到达文件末尾
检查网络连接状态
检查磁盘空间、权限等
修正路径或创建文件
代码修正与预防
修正方案一:使用try-with-resources确保资源关闭
// 错误代码:可能忘记关闭资源publicStringreadFile(String path)throwsIOException{FileReader fr =newFileReader(path);BufferedReader br =newBufferedReader(fr);return br.readLine();// 没有关闭资源,可能造成内存泄漏}// 修正代码:使用try-with-resourcespublicStringreadFile(String path)throwsIOException{try(FileReader fr =newFileReader(path);BufferedReader br =newBufferedReader(fr)){return br.readLine();}// 自动关闭}修正方案二:检查文件存在性
publicvoidprocessFile(String path){File file =newFile(path);if(!file.exists()){System.err.println("文件不存在: "+ path);return;// 或者抛出更友好的异常}try(BufferedReader br =newBufferedReader(newFileReader(file))){// 处理文件}catch(IOException e){System.err.println("读取文件时发生错误: "+ e.getMessage()); e.printStackTrace();}}修正方案三:多层异常处理
publicvoidcopyFile(String src,String dest){try(FileInputStream in =newFileInputStream(src);FileOutputStream out =newFileOutputStream(dest)){byte[] buffer =newbyte[1024];int length;while((length = in.read(buffer))>0){ out.write(buffer,0, length);}}catch(FileNotFoundException e){System.err.println("源文件不存在或目标目录无法写入: "+ e.getMessage());}catch(IOException e){System.err.println("复制过程中发生IO错误: "+ e.getMessage());}}预防措施:
- 始终使用try-with-resources或确保finally中关闭资源
- 操作前检查文件和目录状态
- 为IO操作提供有意义的错误消息
- 考虑重试机制,特别是网络相关的IO操作
2.9 ClassNotFoundException(类未找到异常)
现象描述
当应用程序试图通过字符串名加载类,但在类路径中找不到该类的定义时抛出。
出现场景
场景一:Class.forName()加载类
Class.forName("com.mysql.jdbc.Driver");// 如果驱动jar不在类路径中,抛出异常场景二:类加载器加载类
ClassLoader.getSystemClassLoader().loadClass("com.example.MissingClass");场景三:使用反射创建实例
Object obj =Class.forName("com.example.DynamicClass").newInstance();堆栈分析示例
java.lang.ClassNotFoundException: com.mysql.jdbc.Driver at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:476) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589) at java.base/java.lang.Class.forName0(Native Method) at java.base/java.lang.Class.forName(Class.java:398) 排查方法
是
否
发现ClassNotFoundException
查看缺少的类名
确认该类属于哪个库/JAR
该类是否应该存在?
检查类路径配置
检查类名拼写或版本兼容性
添加缺失的JAR到类路径
代码修正与预防
修正方案一:确保JAR包在类路径中
- 对于Maven项目:检查pom.xml中的依赖
- 对于普通项目:确认JAR文件在classpath下
修正方案二:捕获并处理异常
try{Class.forName("com.mysql.jdbc.Driver");}catch(ClassNotFoundException e){// 提供友好的错误信息thrownewRuntimeException("MySQL驱动未找到,请检查是否添加了mysql-connector-java依赖", e);}修正方案三:使用ServiceLoader模式(Java 6+)
ServiceLoader<Driver> drivers =ServiceLoader.load(Driver.class);for(Driver driver : drivers){// 自动发现所有驱动实现}第二课时(下):综合实战与最佳实践(约10分钟)
3.1 复杂异常排查案例
案例:银行转账系统中的异常链
publicclassBankingService{publicvoidtransfer(String fromAccount,String toAccount,double amount)throwsBusinessException{try{Account from = accountRepository.findByNumber(fromAccount);Accountto= accountRepository.findByNumber(toAccount);if(from ==null||to==null){thrownewIllegalArgumentException("账户不存在");} from.withdraw(amount);to.deposit(amount); transactionLog.log(fromAccount, toAccount, amount);}catch(IllegalArgumentException e){thrownewBusinessException("转账参数错误", e);}catch(InsufficientBalanceException e){thrownewBusinessException("余额不足", e);}catch(Exception e){thrownewBusinessException("转账失败,请稍后重试", e);}}}异常排查思路
当看到类似下面的异常堆栈时:
com.example.BusinessException: 转账失败,请稍后重试 at com.example.BankingService.transfer(BankingService.java:45) at com.example.BankingController.main(BankingController.java:18) Caused by: java.sql.SQLException: Connection timed out at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2189) at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:795) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:329) at java.sql.DriverManager.getConnection(DriverManager.java:664) at com.example.AccountRepository.findByNumber(AccountRepository.java:22) ... 5 more 排查步骤:
- 看顶层异常:
BusinessException,但消息太泛化,请稍后重试没有实质信息 - 看Caused by:
SQLException: Connection timed out,这才是真正原因 - 追溯源头:
AccountRepository.java:22处的数据库连接超时 - 根本原因:数据库连接失败
解决方案:
- 检查数据库服务是否运行
- 检查网络连接
- 检查数据库连接池配置
- 添加重试机制
教训:包装异常时不要丢失原始信息,提供具体的错误消息有助于排查。
3.2 异常处理最佳实践总结
3.2.1 捕获特定异常,而不是通用异常
// 不好的做法try{// 业务代码}catch(Exception e){// 捕获所有异常,掩盖了真正的问题}// 好的做法try{// 业务代码}catch(FileNotFoundException e){// 处理文件不存在}catch(IOException e){// 处理其他IO错误}3.2.2 避免空的catch块
// 绝对不要这样做try{riskyOperation();}catch(Exception e){// 空的catch块,异常被吞噬}// 至少记录异常try{riskyOperation();}catch(Exception e){ logger.error("操作失败", e);// 记录日志throw e;// 或者重新抛出}3.2.3 使用try-with-resources自动关闭资源
// Java 7之前的方式FileInputStream fis =null;try{ fis =newFileInputStream("file.txt");// 处理文件}finally{if(fis !=null){try{ fis.close();}catch(IOException e){ e.printStackTrace();}}}// Java 7+ 推荐的方式try(FileInputStream fis =newFileInputStream("file.txt")){// 处理文件}// 自动关闭3.2.4 使用自定义异常增强业务语义
// 自定义业务异常publicclassInsufficientBalanceExceptionextendsException{privatedouble currentBalance;privatedouble requiredAmount;publicInsufficientBalanceException(double current,double required){super(String.format("余额不足:当前余额%.2f,需要%.2f", current, required));this.currentBalance = current;this.requiredAmount = required;}// getters...}// 使用publicvoidwithdraw(double amount)throwsInsufficientBalanceException{if(balance < amount){thrownewInsufficientBalanceException(balance, amount);} balance -= amount;}3.2.5 方法重写时遵守异常声明规则
- 子类方法可以抛出与父类相同的异常、子类异常,或不抛出异常
- 子类方法不能抛出比父类更宽泛的受检异常
classParent{publicvoidprocess()throwsIOException{}}classChildextendsParent{@Overridepublicvoidprocess()throwsFileNotFoundException{}// 允许,FileNotFoundException是IOException的子类// @Override// public void process() throws Exception { } // 不允许,Exception比IOException更宽泛}3.2.6 记录异常时包含上下文信息
try{processOrder(orderId, userId);}catch(OrderException e){// 记录有用的上下文信息 logger.error("处理订单失败: orderId={}, userId={}", orderId, userId, e);throw e;}3.2.7 不要用异常控制正常的程序流程
// 不好的做法:用异常控制流程try{Integer.parseInt(userInput);// 是数字,继续处理}catch(NumberFormatException e){// 不是数字,执行其他逻辑}// 好的做法:使用条件判断if(userInput.matches("\\d+")){int value =Integer.parseInt(userInput);// 是数字,继续处理}else{// 不是数字,执行其他逻辑}3.2.8 异常处理的黄金法则总结
| 原则 | 说明 |
|---|---|
| 精准捕获 | 捕获具体的异常类型,而不是笼统的Exception |
| 绝不吞噬 | 空的catch块是万恶之源,至少要记录日志 |
| 及时释放 | 使用try-with-resources或finally确保资源释放 |
| 保留原始异常 | 包装异常时要把原异常作为cause传入 |
| 提供上下文 | 异常消息要包含有助于排查的信息 |
| 区分异常类型 | 可恢复用受检异常,程序错误用运行时异常 |
| 文档化 | 用javadoc的@throws说明方法可能抛出的异常 |
3.3 Java 7+ 多异常捕获
从Java 7开始,可以使用|在一个catch块中捕获多个异常类型,减少代码重复:
try{// 可能抛出多种异常的代码}catch(IOException|SQLException e){// 统一处理IO和SQL异常 logger.error("数据访问错误", e);throw e;// Java 7+ 支持更精确的重抛类型检查}注意:多异常捕获时,catch参数隐式为final,不能修改。
3.4 异常处理与事务管理
在企业级应用中,异常处理与事务管理密切相关。通常:
- 运行时异常触发事务回滚
- 受检异常不自动触发事务回滚(在Spring中可通过
rollbackFor配置)
@ServicepublicclassAccountService{@Transactional(rollbackFor ={BusinessException.class,RuntimeException.class})publicvoidtransferMoney(String from,Stringto,double amount)throwsBusinessException{try{// 转账逻辑}catch(InsufficientBalanceException e){// 业务异常,触发事务回滚thrownewBusinessException("转账失败", e);}}}课程总结(约5分钟)
知识体系回顾
通过两个课时的学习,我们全面覆盖了:
- 异常基础:体系结构、受检与非受检异常、异常信息解读
- 运行时异常:
NullPointerException:空引用访问 → 前置检查、OptionalArrayIndexOutOfBoundsException:数组越界 → 边界检查、增强for循环ClassCastException:类型转换错误 → instanceof检查、泛型ArithmeticException:算术异常 → 除零检查NumberFormatException:数字格式错误 → 输入清洗、预验证IllegalArgumentException/IllegalStateException:参数/状态错误 → 前置校验
- 受检异常:
IOException及其子类 → try-with-resources、文件存在性检查ClassNotFoundException→ 检查类路径、依赖管理
- 排查方法:堆栈分析、Caused by追踪、异常链理解
- 最佳实践:精准捕获、避免吞噬、及时释放、保留上下文等8大原则
异常排查思维导图
遇到异常时,按以下顺序思考: ┌─────────────────────────────────────┐ │ 1. 看类型:是什么异常?属于哪一类? │ ├─────────────────────────────────────┤ │ 2. 看消息:异常说了什么?有什么线索? │ ├─────────────────────────────────────┤ │ 3. 看堆栈:第一行自己的代码在哪? │ ├─────────────────────────────────────┤ │ 4. 看原因:有Caused by吗?底层是什么? │ ├─────────────────────────────────────┤ │ 5. 想来源:这个值从哪来的?谁传的? │ ├─────────────────────────────────────┤ │ 6. 想方案:怎么修复?如何预防? │ └─────────────────────────────────────┘