目录一、什么是异常打破程序的正常流程常见异常示例二、Java 异常体系结构理清继承关系不混淆概念核心继承结构三大核心类的区别三、异常的处理方式从防御式编程到核心关键字3.1 两种防御式编程思想LBYL事前防御型Look Before You LeapEAFP事后处理型Its Easier to Ask Forgiveness than Permission3.2 手动抛出异常throw语法格式实战示例参数合法性校验throw 使用注意事项3.3 声明异常throws语法格式实战示例文件操作声明编译时异常throws 使用注意事项3.4 捕获并处理异常try-catch语法格式实战示例多异常捕获与处理运行结果try-catch 使用关键注意事项3.5 必执行的代码块finally语法格式核心场景资源释放全新示例测试结果 1输入正确整数测试结果 2输入非整数finally 的特殊注意事项四、异常的处理流程跟着调用栈走理清执行顺序异常处理完整执行流程实战示例异常的向上传播运行结果五、自定义异常贴合业务场景让异常更有意义5.1 自定义异常的实现步骤5.2 实战示例用户登陆业务的自定义异常步骤 1实现自定义异常类步骤 2在业务代码中抛出自定义异常步骤 3调用业务方法处理自定义异常运行结果5.3 自定义异常的选型建议六、异常处理的最佳实践七、总结在 Java 开发的道路上异常处理是绕不开的核心知识点。无论是新手调试代码时遇到的NullPointerException还是开发企业级项目时处理的网络、文件 IO 异常掌握规范的异常处理方式能让我们的代码更健壮、更易维护还能大幅降低线上问题的排查成本。本文将从异常的核心概念出发逐步拆解 Java 异常体系、处理方式、执行流程最后手把手教你实现自定义异常让你彻底吃透 Java 异常处理。一、什么是异常打破程序的正常流程异常是程序运行过程中发生的非正常行为 / 错误状态它会打断程序的正常执行流程需要开发者通过特定方式进行处理。在开发中我们无法避免各类异常场景比如除数为 0 的算术错误、访问数组不存在的下标、调用空对象的方法、读取不存在的文件、网络请求超时等。这些场景无法通过普通的逻辑判断完全规避而 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 ├─ ErrorJVM级别的严重错误无法通过代码处理 │ ├─ StackOverflowError栈溢出错误如递归无终止条件 │ └─ OutOfMemoryError内存溢出错误OOM └─ Exception程序级别的异常开发者可通过代码处理 ├─ 编译时异常受检查异常 Checked Exception编译期必须处理 │ ├─ IOExceptionIO操作相关异常如文件读取、网络请求 │ ├─ SQLException数据库操作相关异常 │ └─ ClassNotFoundException类加载失败异常 └─ 运行时异常非受检查异常 Unchecked Exception运行期才会出现可按需处理 ├─ NullPointerException空指针异常 ├─ ArrayIndexOutOfBoundsException数组越界异常 ├─ ArithmeticException算术异常 └─ ClassCastException类型转换异常三大核心类的区别ErrorJava 虚拟机无法解决的严重问题属于系统级错误一旦发生程序基本无法恢复只能提前预防。比如递归调用无终止条件会导致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在执行操作前对所有可能出现的问题进行充分检查正常流程和错误处理流程混在一起代码可读性差。/** * 事前防御型编程示例用户登陆 */ 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事后处理型Its Easier to Ask Forgiveness than Permission先执行操作遇到问题再捕获处理将正常流程和错误流程分离代码更清晰这也是 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关键字手动抛出一个指定的异常对象。语法格式throw new 异常类名(异常产生的原因);实战示例参数合法性校验/** * throw 手动抛出异常示例获取集合指定下标元素 */ import java.util.List; public class ThrowDemo { public static T T getListElement(ListT 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) { ListString 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关键字在方法声明处声明该方法可能抛出的异常。语法格式修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2... { // 方法体可能抛出异常 }实战示例文件操作声明编译时异常/** * 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-catchthrows只是将异常转移给调用者并未真正处理异常而try-catch是 Java 中处理异常的核心方式能捕获异常并对其进行处理让程序在发生异常后继续执行。语法格式try { // 可能抛出异常的代码块监控区 } catch (异常类型1 异常对象名) { // 处理异常类型1的代码捕获区 } catch (异常类型2 异常对象名) { // 处理异常类型2的代码 } // 可选finally块下文单独讲解实战示例多异常捕获与处理/** * 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块永远无法执行若多个异常的处理逻辑完全相同可使用 ** 竖线 |** 合并捕获简化代码catch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) { System.out.println(处理异常 e.getMessage()); }可以使用Exception捕获所有异常因为Exception是所有程序级异常的父类但不推荐会掩盖具体的异常类型不利于问题排查仅适用于通用的异常兜底处理。3.5 必执行的代码块finally在程序开发中有些代码无论是否发生异常都必须执行比如打开的文件流、数据库连接、网络连接等资源的释放否则会造成资源泄漏。finally块就是为了解决这个问题它配合try-catch使用里面的代码永远会被执行。语法格式try { // 可能抛出异常的代码 } catch (异常类型 e) { // 处理异常的代码 } finally { // 无论是否发生异常都会执行的代码资源释放为主 }核心场景资源释放全新示例/** * 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方法后续代码不再执行。实战示例异常的向上传播/** * 异常处理流程示例异常向上传播 */ 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方法后续代码执行); } }运行结果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实现自定义异常类/** * 自定义异常用户名不存在继承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在业务代码中抛出自定义异常/** * 用户登陆业务类 */ 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调用业务方法处理自定义异常/** * 测试自定义异常用户登陆 */ 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 异常处理有更全面、更深入的理解在实际开发中玩转异常体系
Java入门( 异常 )
目录一、什么是异常打破程序的正常流程常见异常示例二、Java 异常体系结构理清继承关系不混淆概念核心继承结构三大核心类的区别三、异常的处理方式从防御式编程到核心关键字3.1 两种防御式编程思想LBYL事前防御型Look Before You LeapEAFP事后处理型Its Easier to Ask Forgiveness than Permission3.2 手动抛出异常throw语法格式实战示例参数合法性校验throw 使用注意事项3.3 声明异常throws语法格式实战示例文件操作声明编译时异常throws 使用注意事项3.4 捕获并处理异常try-catch语法格式实战示例多异常捕获与处理运行结果try-catch 使用关键注意事项3.5 必执行的代码块finally语法格式核心场景资源释放全新示例测试结果 1输入正确整数测试结果 2输入非整数finally 的特殊注意事项四、异常的处理流程跟着调用栈走理清执行顺序异常处理完整执行流程实战示例异常的向上传播运行结果五、自定义异常贴合业务场景让异常更有意义5.1 自定义异常的实现步骤5.2 实战示例用户登陆业务的自定义异常步骤 1实现自定义异常类步骤 2在业务代码中抛出自定义异常步骤 3调用业务方法处理自定义异常运行结果5.3 自定义异常的选型建议六、异常处理的最佳实践七、总结在 Java 开发的道路上异常处理是绕不开的核心知识点。无论是新手调试代码时遇到的NullPointerException还是开发企业级项目时处理的网络、文件 IO 异常掌握规范的异常处理方式能让我们的代码更健壮、更易维护还能大幅降低线上问题的排查成本。本文将从异常的核心概念出发逐步拆解 Java 异常体系、处理方式、执行流程最后手把手教你实现自定义异常让你彻底吃透 Java 异常处理。一、什么是异常打破程序的正常流程异常是程序运行过程中发生的非正常行为 / 错误状态它会打断程序的正常执行流程需要开发者通过特定方式进行处理。在开发中我们无法避免各类异常场景比如除数为 0 的算术错误、访问数组不存在的下标、调用空对象的方法、读取不存在的文件、网络请求超时等。这些场景无法通过普通的逻辑判断完全规避而 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 ├─ ErrorJVM级别的严重错误无法通过代码处理 │ ├─ StackOverflowError栈溢出错误如递归无终止条件 │ └─ OutOfMemoryError内存溢出错误OOM └─ Exception程序级别的异常开发者可通过代码处理 ├─ 编译时异常受检查异常 Checked Exception编译期必须处理 │ ├─ IOExceptionIO操作相关异常如文件读取、网络请求 │ ├─ SQLException数据库操作相关异常 │ └─ ClassNotFoundException类加载失败异常 └─ 运行时异常非受检查异常 Unchecked Exception运行期才会出现可按需处理 ├─ NullPointerException空指针异常 ├─ ArrayIndexOutOfBoundsException数组越界异常 ├─ ArithmeticException算术异常 └─ ClassCastException类型转换异常三大核心类的区别ErrorJava 虚拟机无法解决的严重问题属于系统级错误一旦发生程序基本无法恢复只能提前预防。比如递归调用无终止条件会导致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在执行操作前对所有可能出现的问题进行充分检查正常流程和错误处理流程混在一起代码可读性差。/** * 事前防御型编程示例用户登陆 */ 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事后处理型Its Easier to Ask Forgiveness than Permission先执行操作遇到问题再捕获处理将正常流程和错误流程分离代码更清晰这也是 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关键字手动抛出一个指定的异常对象。语法格式throw new 异常类名(异常产生的原因);实战示例参数合法性校验/** * throw 手动抛出异常示例获取集合指定下标元素 */ import java.util.List; public class ThrowDemo { public static T T getListElement(ListT 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) { ListString 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关键字在方法声明处声明该方法可能抛出的异常。语法格式修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2... { // 方法体可能抛出异常 }实战示例文件操作声明编译时异常/** * 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-catchthrows只是将异常转移给调用者并未真正处理异常而try-catch是 Java 中处理异常的核心方式能捕获异常并对其进行处理让程序在发生异常后继续执行。语法格式try { // 可能抛出异常的代码块监控区 } catch (异常类型1 异常对象名) { // 处理异常类型1的代码捕获区 } catch (异常类型2 异常对象名) { // 处理异常类型2的代码 } // 可选finally块下文单独讲解实战示例多异常捕获与处理/** * 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块永远无法执行若多个异常的处理逻辑完全相同可使用 ** 竖线 |** 合并捕获简化代码catch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) { System.out.println(处理异常 e.getMessage()); }可以使用Exception捕获所有异常因为Exception是所有程序级异常的父类但不推荐会掩盖具体的异常类型不利于问题排查仅适用于通用的异常兜底处理。3.5 必执行的代码块finally在程序开发中有些代码无论是否发生异常都必须执行比如打开的文件流、数据库连接、网络连接等资源的释放否则会造成资源泄漏。finally块就是为了解决这个问题它配合try-catch使用里面的代码永远会被执行。语法格式try { // 可能抛出异常的代码 } catch (异常类型 e) { // 处理异常的代码 } finally { // 无论是否发生异常都会执行的代码资源释放为主 }核心场景资源释放全新示例/** * 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方法后续代码不再执行。实战示例异常的向上传播/** * 异常处理流程示例异常向上传播 */ 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方法后续代码执行); } }运行结果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实现自定义异常类/** * 自定义异常用户名不存在继承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在业务代码中抛出自定义异常/** * 用户登陆业务类 */ 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调用业务方法处理自定义异常/** * 测试自定义异常用户登陆 */ 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 异常处理有更全面、更深入的理解在实际开发中玩转异常体系