Java异常处理入门
1. 引入:为什么需要异常?
1.1 程序总会出错
写代码时,有些情况是我们无法控制的:
java
int a = 10 / 0; // 除数不能为0
int[] arr = new int[5];
arr[10] = 100; // 数组越界
String s = null;
s.length(); // 空指针
如果没有异常处理,程序会直接崩溃,后面的代码无法执行。
1.2 异常的作用
异常是Java用来处理程序运行时错误的机制。它让程序在出错时能够:
- 捕获错误,避免崩溃
- 给出提示,方便调试
- 执行补救措施
java
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
}
System.out.println("程序继续执行"); // 会执行
2. 异常的体系结构
2.1 继承体系
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error(错误,程序无法处理)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
│
└── java.lang.Exception(异常,程序可以处理)
├── RuntimeException(运行时异常,非受检)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ArithmeticException
│ ├── ClassCastException
│ └── ...
│
└── 其他Exception(编译时异常,受检)
├── IOException
├── SQLException
├── FileNotFoundException
└── ...
2.2 Error vs Exception
| 类型 | 说明 | 是否需要处理 |
|---|---|---|
| Error | JVM内部错误,如内存溢出、栈溢出 | 程序无法处理 |
| Exception | 程序运行中出现的异常 | 程序应该处理 |
2.3 受检异常 vs 非受检异常
| 类型 | 特点 | 例子 |
|---|---|---|
| 受检异常 | 编译时必须处理(try-catch或throws) | IOException、SQLException |
| 非受检异常 | 编译时不强制处理,运行时才出现 | NullPointerException、ArrayIndexOutOfBoundsException |
即:
- 受检异常 = 编译器逼你处理的
- 非受检异常 = 编译器不管,但运行时会崩
3. 异常的处理方式
3.1 try-catch-finally
基本语法:
java
try {
// 可能出异常的代码
} catch (异常类型1 变量名) {
// 处理异常1
} catch (异常类型2 变量名) {
// 处理异常2
} finally {
// 无论是否异常,都会执行
}
完整示例:
java
public class TryCatchDemo {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // 数组越界
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界:" + e.getMessage());
} finally {
System.out.println("finally执行");
}
System.out.println("程序继续运行");
}
}
关于finally:
finally不一定执行(调用System.exit()、JVM崩溃、守护线程强制终止等情况下不执行)。
3.2 多个catch的注意事项
java
// 正确:子类异常在前,父类异常在后
try {
// ...
} catch (NullPointerException e) {
// 子类
} catch (RuntimeException e) {
// 父类
}
// 错误:父类在前,子类永远捕获不到
try {
// ...
} catch (RuntimeException e) {
// 父类(会拦截所有子类异常)
} catch (NullPointerException e) {
// 永远不会执行
}
3.3 try-catch-finally的几种组合
| 组合 | 说明 |
|---|---|
try-catch |
捕获并处理异常 |
try-finally |
不捕获异常,但保证finally执行 |
try-catch-finally |
完整形式 |
try-with-resources |
自动关闭资源(Java 7+) |
3.4 抛出异常:throws
throws:声明方法可能抛出异常,交给调用者处理。
java
public class ThrowsDemo {
// 声明抛出异常,交给调用者处理
public static void readFile(String path) throws FileNotFoundException {
FileReader reader = new FileReader(path);
}
public static void main(String[] args) {
try {
readFile("test.txt");
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
}
}
}
3.5 主动抛出异常:throw
throw:手动抛出异常对象。
java
public class ThrowDemo {
public static void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法:" + age);
}
System.out.println("年龄:" + age);
}
public static void main(String[] args) {
setAge(200); // 抛出IllegalArgumentException
}
}
3.6 throws vs throw
| 维度 | throws | throw |
|---|---|---|
| 位置 | 方法声明后 | 方法体内 |
| 后面跟的 | 异常类名 | 异常对象 |
| 作用 | 声明方法可能抛异常 | 主动抛出异常对象 |
| 数量 | 可跟多个 | 只能一个 |
4. 自定义异常
4.1 为什么要自定义异常?
Java自带的异常虽然多,但业务上可能需要更具体的异常,比如:
- 年龄不合法异常
- 余额不足异常
- 用户名重复异常
4.2 自定义异常的写法
java
// 自定义受检异常(继承Exception)
public class AgeIllegalException extends Exception {
public AgeIllegalException() {
super();
}
public AgeIllegalException(String message) {
super(message);
}
}
// 自定义非受检异常(继承RuntimeException)
public class BalanceException extends RuntimeException {
public BalanceException(String message) {
super(message);
}
}
4.3 使用自定义异常
java
public class UserService {
public static void setAge(int age) throws AgeIllegalException {
if (age < 0 || age > 150) {
throw new AgeIllegalException("年龄必须在0-150之间,当前值:" + age);
}
System.out.println("年龄设置成功:" + age);
}
public static void withdraw(int balance) {
if (balance < 0) {
throw new BalanceException("余额不能为负数:" + balance);
}
}
public static void main(String[] args) {
// 受检异常需要处理
try {
setAge(200);
} catch (AgeIllegalException e) {
System.out.println(e.getMessage());
}
// 非受检异常不需要强制处理
withdraw(-100);
}
}
5. 异常中的常用方法
java
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
// 获取异常信息
String msg = e.getMessage(); // "/ by zero"
// 打印异常堆栈(最常用)
e.printStackTrace(); // 打印完整调用链
// 获取异常类型
String className = e.getClass().getName(); // "java.lang.ArithmeticException"
// 获取异常原因
Throwable cause = e.getCause(); // 引发当前异常的原因
}
6. 异常的传播
异常会沿着调用栈向上传递,直到被捕获:
java
public class ExceptionPropagation {
public static void method3() {
int result = 10 / 0; // 异常发生在这里
}
public static void method2() {
method3(); // 异常向上传播到这里
}
public static void method1() {
method2(); // 异常向上传播到这里
}
public static void main(String[] args) {
try {
method1(); // 在这里捕获异常
} catch (ArithmeticException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
}
}
传播路径:
method3(发生)→ method2 → method1 → main(捕获)
7. 异常链
一个异常可以引发另一个异常:
java
public class ExceptionChain {
public static void main(String[] args) {
try {
try {
throw new ArithmeticException("除数不能为0");
} catch (ArithmeticException e) {
// 包装成另一个异常再抛出
throw new RuntimeException("计算失败", e);
}
} catch (RuntimeException e) {
System.out.println("异常:" + e.getMessage());
System.out.println("原始原因:" + e.getCause().getMessage());
}
}
}
8. 常见异常及原因
| 异常 | 原因 |
|---|---|
NullPointerException |
调用null对象的方法或属性 |
ArrayIndexOutOfBoundsException |
数组索引越界 |
StringIndexOutOfBoundsException |
字符串索引越界 |
ArithmeticException |
算术异常(如除以0) |
ClassCastException |
类型转换失败 |
NumberFormatException |
字符串转数字失败 |
IllegalArgumentException |
参数不合法 |
FileNotFoundException |
文件不存在 |
IOException |
IO操作失败 |
SQLException |
数据库操作失败 |
9. 易错点总结
-
catch顺序错误:父类异常写在子类前面,子类永远捕获不到
-
finally中return:finally中的return会覆盖try/catch中的return
java
try {
return 1;
} finally {
return 2; // 实际返回2
}
-
finally中抛出异常:会覆盖try/catch中的异常
-
受检异常不处理:编译时报错,必须try-catch或throws
-
空catch块:捕获异常后什么都不做,问题被隐藏
-
异常对象用==比较 :应该用
instanceof或getClass()
10. 总结对比
| 对比项 | 受检异常 | 非受检异常 |
|---|---|---|
| 继承 | Exception(非RuntimeException) | RuntimeException |
| 编译时检查 | 是 | 否 |
| 是否强制处理 | 是 | 否 |
| 常见例子 | IOException、SQLException | NullPointerException、ArrayIndexOutOfBoundsException |
| 对比项 | throws | throw |
|---|---|---|
| 位置 | 方法声明 | 方法体 |
| 后面跟的 | 异常类名 | 异常对象 |
| 作用 | 声明可能抛出 | 主动抛出 |