01、Java 异常的处理机制

  • 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-catchthrows无需强制处理
触发原因外部环境问题(如:文件未找到,网络中断)程序逻辑错误(如:空指针)
恢复可能性通常可恢复(如:重试操作通常不可恢复需修复代码
示例IOExceptionSQLExceptionNullPointerException, 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、默认异常处理 的 执行流程


  1. 异常抛出
    • 程序执行过程中抛出一个异常
      • 如:调用null对象的方法触发 NullPointerException。
  2. 查找异常处理器
    • JVM 沿着方法的调用栈(Call Stack)向上回溯,检查是否有匹配的 catch 块。
  3. 未找到异常处理器(如:Thread.setDefaultUncaughtExceptionHandler 其中一种):
    • 如果所有方法未处理该异常(既未捕获,也未声明 throws),JVM 将接管处理。
  4. 执行默认处理
    • 打印异常信息
      • 输出异常类名消息(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 必须至少有一个 catchfinally。
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、执行流程


  1. 执行 try
    • 若代码无异常,执行完 try 后跳至 finally。
    • 若代码抛出异常,立即中断 try 块的执行,跳转至匹配的 catch 块。
  2. 匹配 catch
    • 按 catch 块顺序匹配异常类型,执行第一个匹配的 catch 块。
    • 若异常未被任何 catch 捕获,异常会向上传播(最终可能触发 默认异常处理)。
  3. 执行 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 中的异常会覆盖 trycatch 中的异常
    • 如果 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 块中的异常
    抑制异常: 关闭资源时发生异常
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值