0%

异常

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

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

异常体系结构

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 块用于捕获并处理特定类型的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 块:可捕获多种异常,子类异常需放在父类异常之前(否则子类异常块永远无法执行)。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static int testFinally() {
    try {
    return 1;
    } catch (Exception e) {
    return 2;
    } finally {
    return 3; // 最终返回3
    }
    }

throw:主动抛出异常

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 用于在方法声明中指定该方法可能抛出的异常类型(通常用于检查型异常),告知调用者需处理这些异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 定义自定义异常,用于表示业务相关的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 自定义检查型异常(继承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()),避免 “吞掉” 异常导致调试困难。

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

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

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

欢迎关注我的其它发布渠道