异常

Java 异常处理详解:从原理到实践

异常是程序运行过程中出现的非预期情况(如空指针、数组越界等)。Java 提供了完善的异常处理机制,通过 try-catch-finallythrowthrows 等关键字,使程序在遇到异常时能够优雅地处理,而非直接崩溃。本文将深入解析 Java 异常体系、处理机制及最佳实践。

异常体系结构

Java 异常体系以 Throwable 为根类,分为两大分支:Error(错误)Exception(异常),二者均继承自 Throwable

Throwable
├─ Error(错误):JVM 无法处理的严重问题
│  ├─ VirtualMachineError(虚拟机错误)
│  │  ├─ OutOfMemoryError(内存溢出)
│  │  └─ StackOverflowError(栈溢出)
│  └─ NoClassDefFoundError(类定义未找到)
│
└─ Exception(异常):程序可处理的问题
   ├─ RuntimeException(运行时异常,非检查型)
   │  ├─ NullPointerException(空指针)
   │  ├─ ClassCastException(类型转换)
   │  ├─ ArithmeticException(算术错误)
   │  └─ IndexOutOfBoundsException(下标越界)
   │
   └─ 非 RuntimeException(检查型异常)
      ├─ IOException(I/O 异常)
      ├─ ClassNotFoundException(类未找到)
      └─ SQLException(数据库异常)

1. Error(错误)

Error 表示 JVM 自身无法解决的严重问题,通常由硬件或系统级故障导致,程序无法处理,应避免捕获 Error

  • 常见类型:
    • OutOfMemoryError:内存溢出(如无限创建对象)。
    • StackOverflowError:栈溢出(如无限递归调用)。
    • NoClassDefFoundError:类定义缺失(如编译后删除了 .class 文件)。

2. Exception(异常)

Exception 表示程序运行中可预期的异常,程序可以捕获并处理,分为两类:

(1)运行时异常(RuntimeException,非检查型异常)
  • 特点:编译期不强制要求处理(可捕获也可不捕获),通常由编程错误导致。
  • 常见类型:
    • NullPointerException:调用 null 对象的方法。
    • ClassCastException:非法类型转换(如 Object o = "abc"; Integer i = (Integer) o;)。
    • ArithmeticException:算术错误(如除以 0)。
    • IndexOutOfBoundsException:数组或集合下标越界。
(2)检查型异常(非 RuntimeException
  • 特点:编译期强制要求处理(必须捕获或通过 throws 声明),通常由外部环境导致(如 I/O 错误)。
  • 常见类型:
    • IOException:文件读写错误(如读取不存在的文件)。
    • ClassNotFoundException:加载类时未找到类定义。
    • SQLException:数据库操作异常(如连接失败)。

异常处理核心机制

Java 通过 try-catch-finally 块捕获和处理异常,配合 throwthrows 关键字主动抛出异常,形成完整的异常处理流程。

try-catch:捕获并处理异常

try 块包含可能抛出异常的代码,catch 块用于捕获并处理特定类型的异常。

public class TryCatchDemo {
    public static void main(String[] args) {
        String str = "123a";
        try {
            // 可能抛出NumberFormatException(运行时异常)
            int num = Integer.parseInt(str);
            System.out.println("转换结果:" + num);
        } catch (NumberFormatException e) {
            // 处理异常:打印错误信息或执行恢复逻辑
            System.out.println("转换失败:" + e.getMessage()); // 输出:For input string: "123a"
            e.printStackTrace(); // 打印堆栈跟踪(便于调试)
        }
        System.out.println("程序继续执行"); // 异常处理后,程序继续运行
    }
}
  • catch 块:可捕获多种异常,子类异常需放在父类异常之前(否则子类异常块永远无法执行)。

    try {
        // 可能抛出多种异常的代码
    } catch (NullPointerException e) {
        // 处理空指针异常
    } catch (NumberFormatException e) {
        // 处理格式转换异常
    } catch (Exception e) { // 父类异常放在最后
        // 处理其他异常
    }

finally:释放资源(必执行代码)

finally 块用于执行无论是否发生异常都必须执行的代码(如关闭文件、释放数据库连接等资源)。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class FinallyDemo {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            // 读取文件操作(可能抛出IOException)
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 无论是否异常,均关闭流(释放资源)
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("finally块执行"); // 必执行
        }
    }
}
  • finallyreturn 的关系

    • trycatch 块中有 returnfinally 块仍会执行(在 return 前执行)。
    • finally 块中也有 return,则最终返回 finally 中的值(覆盖 try/catchreturn)。
    public static int testFinally() {
        try {
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            return 3; // 最终返回3
        }
    }

throw:主动抛出异常

throw 用于在方法内部主动抛出异常(通常用于检测到非法状态时),抛出的是异常对象

public class ThrowDemo {
    public static void checkAge(int age) {
        if (age < 0 || age > 150) {
            // 主动抛出IllegalArgumentException(运行时异常)
            throw new IllegalArgumentException("年龄必须在0-150之间:" + age);
        }
        System.out.println("年龄合法:" + age);
    }

    public static void main(String[] args) {
        try {
            checkAge(200); // 调用可能抛出异常的方法
        } catch (IllegalArgumentException e) {
            e.printStackTrace(); // 捕获并处理异常
        }
    }
}

throws:声明方法可能抛出的异常

throws 用于在方法声明中指定该方法可能抛出的异常类型(通常用于检查型异常),告知调用者需处理这些异常。

import java.io.IOException;

// 方法声明抛出IOException(检查型异常)
public static void readFile(String path) throws IOException {
    if (!new File(path).exists()) {
        // 抛出检查型异常,必须通过throws声明
        throw new IOException("文件不存在:" + path);
    }
    // 读取文件...
}

public static void main(String[] args) {
    try {
        readFile("nonexistent.txt"); // 调用声明了异常的方法,必须处理
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 注意:
    • 运行时异常(RuntimeException)可不用 throws 声明(但建议明确抛出)。
    • 子类重写父类方法时,throws 声明的异常不能比父类更宽泛(可更少或相同)。

自定义异常

除了 Java 内置异常,还可通过继承 ExceptionRuntimeException 定义自定义异常,用于表示业务相关的错误。

// 自定义检查型异常(继承Exception)
class AgeOutOfRangeException extends Exception {
    public AgeOutOfRangeException(String message) {
        super(message); // 调用父类构造器,保存异常信息
    }
}

// 自定义运行时异常(继承RuntimeException)
class NameEmptyException extends RuntimeException {
    public NameEmptyException(String message) {
        super(message);
    }
}

// 使用自定义异常
public class CustomExceptionDemo {
    public static void validateUser(String name, int age) throws AgeOutOfRangeException {
        if (name == null || name.isEmpty()) {
            throw new NameEmptyException("姓名不能为空"); // 运行时异常,无需throws
        }
        if (age < 0 || age > 150) {
            throw new AgeOutOfRangeException("年龄非法:" + age); // 检查型异常,需throws
        }
    }

    public static void main(String[] args) {
        try {
            validateUser("", 200);
        } catch (NameEmptyException e) {
            System.out.println("处理姓名异常:" + e.getMessage());
        } catch (AgeOutOfRangeException e) {
            System.out.println("处理年龄异常:" + e.getMessage());
        }
    }
}

异常处理最佳实践

  1. 避免捕获 ExceptionThrowable:过度宽泛的捕获会掩盖真正的错误(如 OutOfMemoryError),应捕获具体异常。

  2. 不要忽略异常catch 块中至少打印异常信息(e.printStackTrace()),避免 “吞掉” 异常导致调试困难。

    // 错误示例:忽略异常
    try { ... } catch (Exception e) {}
  3. 优先使用 try-with-resources 自动关闭资源(Java 7+):对于实现 AutoCloseable 接口的资源(如流、数据库连接),无需手动在 finally 中关闭。

    // 自动关闭资源(无需finally)
    try (FileInputStream fis = new FileInputStream("test.txt")) {
        // 读取文件...
    } catch (IOException e) {
        e.printStackTrace();
    }
  4. 区分检查型与运行时异常

    • 检查型异常:用于外部环境导致的错误(如文件不存在),需强制处理。
    • 运行时异常:用于编程错误(如空指针),应通过代码规范避免(而非捕获)。
  5. 异常信息要具体:异常消息应包含足够的上下文(如参数值、操作步骤),便于排查问题