- Java 中的异常处理机制是一种强大的错误管理工具。
- 它允许程序在运行时 检测和处理 错误(异常)。
- 从而防止程序意外崩溃,提高代码的健壮性和可靠性。
- 其核心思想是 “将错误处理代码与常规业务逻辑代码分离”。
一、Java 异常的分类体系
- Java 的异常分类体系基于 Throwable 类,所有错误和异常都是其子类。
- 体系结构清晰,分为 错误(Error) 和 异常(Exception) 两大类,
- 异常又分为 检查型异常(Checked Exception)和 非检查型异常(Unchecked Exception)
1、Throwable 类(顶级父类)
- 所有错误和异常的根类,提供异常信息的通用方法。
- 如:getMessage(), printStackTrace()。
- 直接子类:
- Error:严重系统错误,程序一般无法处理。
- 属于程序无法处理的错误 ,没办法通过 catch 来进行捕获 。
- Exception:程序逻辑相关的异常,可捕获处理。
- 程序本身可以处理的异常,可以通过 catch 来进行捕获。
2、Error(错误)
- 描述:由 JVM 或 底层系统引发的严重问题,程序通常无法恢复。
- 如:内存不足、系统崩溃。
- 特点:
- 属于非检查型异常(Unchecked),无需显式捕获。
- 应用程序一般无法处理,应避免捕获。
- 一般只能重启服务。
- 常见子类:
- OutOfMemoryError:内存耗尽。
- StackOverflowError:栈溢出。
- NoClassDefFoundError:类定义缺失。
3、Exception(异常)
- 描述:程序逻辑中的可预期问题,开发者需处理。
- 分类:
- 检查型异常(Checked Exception)和 非检查型异常(Unchecked Exception)。
特性 | 检查型异常(Checked) | 非检查型异常(Unchecked) |
---|---|---|
继承关系 | 直接继承 Exception | 继承 RuntimeException 或 Error |
处理要求 | 必须显式处理(try-catch或throws) | 无需强制处理 |
触发原因 | 外部环境问题(如:文件未找到,网络中断) | 程序逻辑错误(如:空指针) |
恢复可能性 | 通常可恢复(如:重试操作) | 通常不可恢复(需修复代码) |
示例 | IOException,SQLException | NullPointerException, ArrayIndexOutOfBoundsException |
4、检查型异常(Checked Exception)/ 编译时异常
- 编译时异常:
- 继承自Exception,编译阶段会报错,必须在编程时进行处理。否则,就会编译通不过。
- 必须显式处理:try-catch 处理 或 throws 声明。
- 通常由 外部因素导致(如:文件不存在、网络中断)。
- 常见子类:
- IOException(输入输出异常)
- SQLException(数据库操作异常)
- ClassNotFoundException(类未找到)
- Exception 异常类及其子类(除去 RuntimeException 异常类及其子类)都是检查时异常。
5、非检查型异常(Unchecked Exception)/ 运行时异常
- 运行时异常,继承自 RuntimeException,编译阶段不会报错,无需显式处理(但可捕获)。
- 通常由 程序逻辑错误 导致(如:空指针、数组越界)。
- 常见子类:
- NullPointerException(空指针)
- ArrayIndexOutOfBoundsException(数组越界)
- IllegalArgumentException(非法参数)
- ArithmeticException(算术错误,如:除以零)
6、自定义异常
- 检查型异常:继承 Exception。
// 自定义检查型异常
public class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
- 非检查型异常:继承 RuntimeException。
// 自定义非检查型异常
public class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
7、异常处理的最佳实践
- 检查型异常:用于 外部因素 导致的,可恢复场景(如:用户输入错误、文件未找到,网络中断)。
- 非检查型异常:用于 程序逻辑错误(如:参数校验失败)。
- 避免滥用 RuntimeException:逻辑错误应通过代码修复,而非依赖异常处理。
- 不要忽略异常:至少记录异常信息(如:日志)。
- 优先使用标准异常:如:IllegalArgumentException 代替自定义参数错误。
8、NoClassDefFoundError 和 ClassNotFoundException 区别
- NoClassDefFoundError:
- 属于 Error(java.lang.Error 的子类)。
- 表示 JVM 隐式加载类时,未找到定义。
- 如:通过 new A() 加载类。
- 触发场景:
- 编译时存在某个类,在编译后被删除或未部署,导致运行时类路径(Classpath)中缺失。
- 类初始化失败(如:静态代码块抛出异常、静态变量初始化异常),导致后续无法加载。
- 依赖冲突。
package org.rainlotus.materials.javabase.a05_exception;
public class A {
static { System.out.println("A 类的 静态代码块"); }
}
- 编译后,将 A.class 删除
package org.rainlotus.materials.javabase.a05_exception;
public class NoClassDefFoundErrorTest {
public static void main(String[] args) {
new A();
}
}
- 运行结果:
# zhangxw @ zhangxiangdeMBP in ~/IdeaProjects/rain-lotus/materials-javabase/target/classes on git:master x [17:55:40]
$ java org.rainlotus.materials.javabase.a05_exception.NoClassDefFoundErrorTest
Exception in thread "main" java.lang.NoClassDefFoundError: org/rainlotus/materials/javabase/a05_exception/A
at org.rainlotus.materials.javabase.a05_exception.NoClassDefFoundErrorTest.main(NoClassDefFoundErrorTest.java:5)
Caused by: java.lang.ClassNotFoundException: org.rainlotus.materials.javabase.a05_exception.A
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
... 1 more
- ClassNotFoundException:
- 属于 Exception(java.lang.Exception 的子类)。
- 表示显式加载类时,未找到定义。
- 如:通过 Class.forName()、ClassLoader.loadClass() 加载类。
- 触发场景:
- 显式通过 类名加载类 时,类路径中不存在该类.
- 如:路径写错了、编译时存在某个类,在编译后被删除或未部署。
package org.rainlotus.materials.javabase.a05_exception;
public class A {
static { System.out.println("A 类的 静态代码块"); }
}
- 编译后,将 A.class 删除
package org.rainlotus.materials.javabase.a05_exception;
public class ClassNotFoundExceptionTest {
public static void main(String[] args) {
try {
Class.forName("org.rainlotus.materials.javabase.a05_exception.A");
} catch (ClassNotFoundException e) {
e.printStackTrace(); // 显式捕获处理
}
}
}
- 运行结果:
# zhangxw @ zhangxiangdeMBP in ~/IdeaProjects/rain-lotus/materials-javabase/target/classes on git:master x [17:55:45] C:1
$ java org.rainlotus.materials.javabase.a05_exception.ClassNotFoundExceptionTest
java.lang.ClassNotFoundException: org.rainlotus.materials.javabase.a05_exception.A
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:421)
at java.base/java.lang.Class.forName(Class.java:412)
at org.rainlotus.materials.javabase.a05_exception.ClassNotFoundExceptionTest.main(ClassNotFoundExceptionTest.java:6)
二、异常处理的方式 – 默认异常处理
默认异常处理(Default Exception Handling) :
- 是指当程序抛出异常,但是代码中没有通过 try-catch 块或 throws 的显式处理时,
- Java 虚拟机(JVM)自动执行的异常处理逻辑。
核心作用:
- JVM 提供的一种“兜底”策略,确保程序不会因未捕获的异常而完全崩溃,同时提供调试信息。
1、JVM 触发 默认异常处理的场景
- 1、程序运行时,抛出异常,程序抛出一个异常(如:NullPointerException)。
- 2、代码中
- 没有显式的 try-catch 块捕获该异常。
- 方法签名中没有通过 throws 声明向上传递该异常。
2、默认异常处理 的 执行流程
- 异常抛出:
- 程序执行过程中抛出一个异常。
- 如:调用null对象的方法触发 NullPointerException。
- 查找异常处理器:
- JVM 沿着方法的调用栈(Call Stack)向上回溯,检查是否有匹配的 catch 块。
- 未找到异常处理器(如:Thread.setDefaultUncaughtExceptionHandler 其中一种):
- 如果所有方法均未处理该异常(既未捕获,也未声明 throws),JVM 将接管处理。
- 执行默认处理:
- 打印异常信息:
- 输出异常的类名、消息(message)及堆栈跟踪(stack trace)。
- 终止当前线程:
- 默认情况下,仅终止发生异常的线程,其它线程继续运行(若是多线程程序)。
- 示例:
public class DefaultExceptionDemo {
public static void main(String[] args) {
// 触发 NullPointerException
String str = null;
System.out.println(str.length()); // 此处抛出异常
}
}
- 运行结果:
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "str" is null
at DefaultExceptionDemo.main(DefaultExceptionDemo.java:4)
说明:
- JVM 检测到未捕获的 NullPointerException,触发默认处理。
- 输出异常类型、消息及堆栈跟踪(显示异常发生的位置)。
- main 线程终止,程序退出。
3、默认异常处理的局限性
- 用户体验差:
- 直接打印堆栈信息对普通用户不友好。可能导致程序突然终止。
- 无法恢复程序:
- 默认处理仅终止线程,无法执行自定义的恢复逻辑(如:重试操作、资源清理)。
- 不适用于生产环境:
- 生产环境中需避免依赖默认处理,应通过显式捕获异常来保证程序健壮性。
4、如何覆盖默认异常处理?
- 1、全局 未捕获 异常处理器
- 通过 Thread.setDefaultUncaughtExceptionHandler 设置全局处理器。
- 自定义未捕获异常的处理逻辑。
public class CustomExceptionHandler {
public static void main(String[] args) {
// 设置全局默认异常处理器
Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
System.err.println("全局捕获到未处理的异常!");
System.err.println("线程: " + thread.getName());
System.err.println("异常: " + exception.getMessage());
});
// 触发异常
throw new RuntimeException("测试异常");
}
}
- 2、线程级异常处理器
- 为单个线程设置未捕获异常处理器。
Thread thread = new Thread(() -> {
throw new RuntimeException("线程异常");
});
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("线程 " + t.getName() + " 发生异常: " + e);
});
thread.start();
三、异常处理的方式 – throws + 异常类型
- 在 Java 中,throws + 异常类型 是一种将异常向上抛出的声明机制。
- 将方法内部可能抛出的检查型异常(Checked Exception)声明在方法签名中。
- throws 不处理异常,强制调用者处理这些异常。
- 调用者必须按约定处理这些异常(如:捕获或继续抛出)。
1、throws 的语法
- 在方法签名后添加 throws 关键字,后接一个或多个异常类型(用逗号分隔):
public void methodName() throws ExceptionType1, ExceptionType2 {
// 可能抛出 ExceptionType1 或 ExceptionType2 的代码
}
- 示例:
// 读取文件可能抛出 IOException(检查型异常)
public void readFile() throws IOException {
FileReader file = new FileReader("test.txt");
}
- 调用者必须通过 throws 声明或 try-catch 处理这些异常。
public class Main {
public static void main(String[] args) {
try {
new FileReader().readFile();
} catch (IOException e) {
System.out.println("文件读取失败:" + e.getMessage());
}
}
}
2、适用场景
- 检查型异常(Checked Exception):必须通过 throws 声明或 try-catch 处理。
- 如:IOException, SQLException。
- 非检查型异常(Unchecked Exception):无需声明,但也可声明(通常不推荐)。
- 如:NullPointerException, IllegalArgumentException。
3、throws 与 throw 的区别
关键字 | 作用 | 示例 |
---|---|---|
throws | 声明方法可能抛出的 异常类型 | public void read() throws IOException |
throw | 在代码中手动抛出异常对象 | throw new IOException(“文件损坏”) |
4、throws vs try-catch 的选择
场景 | 使用 throws | 使用 try-catch |
---|---|---|
异常处理责任 | 调用者处理 | 当前方法直接处理 |
适用层级 | 底层工具方法 | 业务逻辑层 |
代码可读性 | 方法签名明确异常类型 | 内部处理逻辑清晰 |
恢复可能性 | 调用者决定是否恢复 | 当前方法直接尝试恢复 |
四、异常处理的方式 – try-catch-finally
- 在 Java 中,try-catch-finally 是处理异常的核心机制。
- 用于捕获、处理代码中的异常,并确保资源释放等清理操作 可靠执行。
1、基本语法
- 完整的结构:
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 类型的异常
} finally {
// 无论是否发生异常,最终执行的代码(资源清理等)
}
- 不完整的合法结构:
- try 必须至少有一个 catch 或 finally。
try { ... } finally { ... } // 无 catch,但需声明异常
try { ... } catch (Exception e) // 无 finally
2、Java7 中 try-with-reources 语法
- 1、其主要目的是,精简资源打开和关闭的方式。
- 每个资源的打开和关闭都需要 try-finally,如果有多个资源,会导致非常繁琐。
- 2、在字节码层面自动使用 Supressed 异常。
/**
* 最原始的文件拷贝方式
*
* @param sourceFile 源文件路径
* @param targetFile 目标文件路径
*/
public static void copyFileUseBio(String sourceFile, String targetFile){
try (
// 1、根据文件路径创建文件的输入流和输出流
FileInputStream inputStream = new FileInputStream(sourceFile);
FileOutputStream outputStream = new FileOutputStream(targetFile);
) {
byte[] buffer = new byte[64];
int length = 0;
while (true){
length = inputStream.read(buffer);
if(-1 == length){
break;
}
outputStream.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
- 编译之后的代码:
- 能看到调用 var5.addSuppressed(var30); 方法。
public static void copyFileUseBio(String sourceFile, String targetFile) {
try {
FileInputStream inputStream = new FileInputStream(sourceFile);
Throwable var3 = null;
try {
FileOutputStream outputStream = new FileOutputStream(targetFile);
Throwable var5 = null;
try {
byte[] buffer = new byte[64];
boolean var7 = false;
while(true) {
int length = inputStream.read(buffer);
if (-1 == length) {
break;
}
outputStream.write(buffer, 0, length);
}
} catch (Throwable var31) {
var5 = var31;
throw var31;
} finally {
if (outputStream != null) {
if (var5 != null) {
try {
outputStream.close();
} catch (Throwable var30) {
var5.addSuppressed(var30);
}
} else {
outputStream.close();
}
}
}
} catch (Throwable var33) {
var3 = var33;
throw var33;
} finally {
if (inputStream != null) {
if (var3 != null) {
try {
inputStream.close();
} catch (Throwable var29) {
var3.addSuppressed(var29);
}
} else {
inputStream.close();
}
}
}
} catch (IOException var35) {
var35.printStackTrace();
}
}
3、执行流程
- 执行 try 块:
- 若代码无异常,执行完 try 后跳至 finally。
- 若代码抛出异常,立即中断 try 块的执行,跳转至匹配的 catch 块。
- 匹配 catch 块:
- 按 catch 块顺序匹配异常类型,执行第一个匹配的 catch 块。
- 若异常未被任何 catch 捕获,异常会向上传播(最终可能触发 默认异常处理)。
- 执行 finally 块:
- 无论是否发生异常,finally 块始终执行(除非 JVM 退出,如 System.exit())。
4、关键规则
- 规则 1:finally 块始终执行(除非 JVM 退出,如 System.exit())
- 1、任何情况(包括:return、break、continue)下,
- 2、无论是否发生异常 。
public int example() {
try {
return 1; // 返回值暂存,执行 finally 后返回
} finally {
System.out.println("finally 执行");
}
}
// 输出:finally 执行 → 返回 1
- 编译后的代码
public int example() {
byte var1;
try {
var1 = 1;
} finally {
System.out.println("finally 执行");
}
return var1;
}
- 规则 2:catch 块顺序由具体到宽泛
- 子类异常在前,父类异常在后,否则编译报错。
try {
// ...
} catch (IOException e) { // 子类
// 处理 IO 异常
} catch (Exception e) { // 父类
// 处理其他异常
}
- 规则 3:try 必须至少有一个 catch 或 ****finally
- 以下写法合法:
try { ... } finally { ... } // 无 catch,但需声明异常
try { ... } catch (Exception e) // 无 finally
5、常见使用场景
- 场景 1:资源释放
- 确保文件、数据库连接等资源被关闭,避免泄漏。
FileInputStream file = null;
try {
file = new FileInputStream("test.txt");
// 读取文件
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) {
try {
file.close(); // 确保资源关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 场景 2:异常分类处理
- 对不同类型异常进行差异化处理。
try {
// 抛出 NumberFormatException
int num = Integer.parseInt("abc");
// 可能抛出 FileNotFoundException
FileReader file = new FileReader("test.txt");
} catch (NumberFormatException e) {
System.out.println("数字格式错误");
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
}
6、注意事项
- 注意 1:finally 中避免 return
- finally 中的 return 会覆盖 try 或 catch 的返回值:
public int example() {
try {
return 1;
} finally {
return 2; // 最终返回 2
}
}
- 注意 2:finally 中的异常会覆盖原异常
- 若 try 和 finally 都抛异常,finally 的异常会替代原异常:
try {
throw new IOException("IO 错误");
} finally {
throw new RuntimeException("finally 异常"); // 最终抛出 RuntimeException
}
- 注意 3:Java 7+ 使用 try-with-resources
- 替代 手动关闭资源,自动调用资源的 close() 方法。
- 注意:FileInputStream 需实现 AutoCloseable 接口。
try (FileInputStream file = new FileInputStream("test.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
}
6、最佳实践
- 优先捕获具体异常:
- 避免宽泛的 catch (Exception e),防止隐藏问题。
- 资源管理推荐 try-with-resources:
- 简化代码,避免手动关闭资源的繁琐。
- finally 中仅做清理操作:
- 避免业务逻辑或复杂计算。
- 记录异常信息:
- 在catch块中使用日志工具(如:log4j)记录异常,而非仅 printStackTrace()。
7、finally 代码块 不被执行 的情况有哪些?
- 1、程序直接终止(JVM 退出)
- 如果 try 或 catch 块中调用了 System.exit()。
- JVM 会立即终止,finally 块不会执行。
try {
System.out.println("执行 try 块");
System.exit(0); // 强制终止 JVM
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally 块"); // 不会执行
}
- 2、守护线程(Daemon Thread)被终止
- 如果 finally 块所在的线程是守护线程(Daemon Thread)。
- 当所有非守护线程结束时,JVM 会立即退出,可能导致守护线程的 finally 块未执行。
Thread daemonThread = new Thread(() -> {
try {
System.out.println("守护线程执行 try 块");
} finally {
System.out.println("守护线程的 finally 块"); // 可能不会执行
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
// 主线程结束,JVM 可能直接退出,不等待守护线程的 finally 块
- 3、JVM 崩溃或底层错误
- 如果 try 或 catch 块中触发了 JVM 内部错误(如:内存不足、栈溢出等)。
- JVM 可能无法正常执行 finally 块。
try {
// 触发 JVM 内部错误(如:通过反射破坏 JVM)
sun.misc.Unsafe.getUnsafe().putAddress(0, 0);
} finally {
System.out.println("finally 块"); // 不会执行
}
- 4、无限阻塞或死循环
- 若 try 或 catch 块中存在无限阻塞且没有外部干预,finally 块可能永远不会执行。
- 如:死锁、无限循环、Thread.suspend()。
try {
System.out.println("进入 try 块");
while (true) { // 无限循环
// 无退出条件
}
} finally {
System.out.println("finally 块"); // 不会执行
}
- 5、操作系统强制终止进程
- 如果操作系统直接杀死 Java 进程(如:通过 kill -9 命令),finally 块无法执行。
- 6、线程被强制终止
- 如果线程通过已废弃的 Thread.stop() 方法被强制终止,finally 块可能无法执行。
Thread thread = new Thread(() -> {
try {
System.out.println("线程执行 try 块");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally 块"); // 可能不会执行
}
});
thread.start();
Thread.sleep(100);
thread.stop(); // 强制终止线程(已废弃方法!)
8、如果 finally 中出现异常,会怎么样?
- 会 中断 finally 的执行,然后抛出异常。
- 1、finally 中的异常会覆盖 try 或 catch 中的异常
- 如果 try 或 catch 块中已经抛出异常,而 finally 块中又抛出新的异常。
- 最终抛出的异常是 finally 中的异常,原始异常会被抑制(Suppressed)。
public class FinallyExceptionDemo {
public static void main(String[] args) {
try {
try {
throw new RuntimeException("来自 try 块的异常");
} finally {
throw new RuntimeException("来自 finally 块的异常");
}
} catch (RuntimeException e) {
System.out.println("捕获的异常: " + e.getMessage());
}
}
}
// 捕获的异常: 来自 finally 块的异常
- 2、finally 中的异常会直接终止程序(如果未捕获)
- 如果 finally 中的异常未被捕获(即:没有外层的 try-catch)。
- 程序会终止,并打印异常堆栈。
public static void main(String[] args) {
try {
System.out.println("执行 try 块");
} finally {
throw new RuntimeException("finally 中的异常");
}
}
/* 输出:
执行 try 块
Exception in thread "main" java.lang.RuntimeException: finally 中的异常
at FinallyExceptionDemo.main(FinallyExceptionDemo.java:5) */
- 3、异常抑制机制(Java 7+)
- 1、自动调用 Throwable.addSuppressed():
- 在 try-with-resources 中,如果 try 块和 close() 方法均抛出异常。
- close() 的异常会被标记为“抑制异常”,主异常仍是 try 块的异常。
- 2、手动去获取被抑制异常:
- 可通过 Throwable.getSuppressed() 获取所有被抑制的异常。
try (AutoCloseable resource = () -> {
throw new IOException("关闭资源时发生异常");
}) {
throw new RuntimeException("try 块中的异常");
} catch (Exception e) {
System.out.println("主异常: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("抑制异常: " + suppressed.getMessage());
}
}
/* 输出:
主异常: try 块中的异常
抑制异常: 关闭资源时发生异常
*/