异常
Java 异常处理详解:从原理到实践
异常是程序运行过程中出现的非预期情况(如空指针、数组越界等)。Java 提供了完善的异常处理机制,通过 try-catch-finally、throw、throws 等关键字,使程序在遇到异常时能够优雅地处理,而非直接崩溃。本文将深入解析 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 块捕获和处理异常,配合 throw 和 throws 关键字主动抛出异常,形成完整的异常处理流程。
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块执行"); // 必执行
}
}
}
finally与return的关系:- 若
try或catch块中有return,finally块仍会执行(在return前执行)。 - 若
finally块中也有return,则最终返回finally中的值(覆盖try/catch的return)。
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 内置异常,还可通过继承 Exception 或 RuntimeException 定义自定义异常,用于表示业务相关的错误。
// 自定义检查型异常(继承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());
}
}
}
异常处理最佳实践
避免捕获
Exception或Throwable:过度宽泛的捕获会掩盖真正的错误(如OutOfMemoryError),应捕获具体异常。不要忽略异常:
catch块中至少打印异常信息(e.printStackTrace()),避免 “吞掉” 异常导致调试困难。// 错误示例:忽略异常 try { ... } catch (Exception e) {}优先使用
try-with-resources自动关闭资源(Java 7+):对于实现AutoCloseable接口的资源(如流、数据库连接),无需手动在finally中关闭。// 自动关闭资源(无需finally) try (FileInputStream fis = new FileInputStream("test.txt")) { // 读取文件... } catch (IOException e) { e.printStackTrace(); }区分检查型与运行时异常:
- 检查型异常:用于外部环境导致的错误(如文件不存在),需强制处理。
- 运行时异常:用于编程错误(如空指针),应通过代码规范避免(而非捕获)。
异常信息要具体:异常消息应包含足够的上下文(如参数值、操作步骤),便于排查问题