Java 异常处理入门:看懂这篇,程序再也不 "崩溃" 了
刚学 Java 的你,是不是也遇到过这种情况:写了一个看似完美的程序,输入 10 和 2 能正常出结果,可一旦输入 10 和 0,程序就突然报错终止,屏幕上一堆看不懂的英文?其实这就是 Java 里的 "异常" 在搞鬼。今天就用通俗的比喻 + 实战代码,带你彻底搞懂异常处理,让你的程序从此更稳健~
一、先搞懂:异常和错误不是一回事
我们可以把程序运行比作一次探险旅程,清晰区分两种 "意外状况":
1. 异常(Exception):能解决的小麻烦
旅途中难免遇到小意外,但只要提前准备,就能顺利化解,旅程还能继续:
-
想过河发现桥断了(对应
IOException,比如读取文件失败) -
打开地图发现是空白(对应
NullPointerException,空指针异常) -
计算路程时不小心除以零(对应
ArithmeticException,算术异常)这些情况都是可以预见和处理的,就像绕路、重新找地图一样,处理后程序能正常运行。
2. 错误(Error):挡不住的大灾难
如果遇到毁灭性事件,再怎么准备也没用,只能终止旅程:
-
突然地震,地面裂开鸿沟(对应
OutOfMemoryError,内存溢出) -
探险队全员病倒(对应
NoClassDefFoundError,类定义未找到)这类问题是 JVM 或系统层面的严重故障,程序无法捕获和处理,只能提前预防。
一张表分清异常和错误
| 特性 | 异常 (Exception) | 错误 (Error) |
|---|---|---|
| 本质 | 程序运行中的意外情况 | JVM / 系统级严重故障 |
| 处理方式 | 可以用代码捕获处理 | 无法处理,只能预防 |
| 影响范围 | 不处理会导致当前线程终止 | 通常导致整个程序崩溃 |
| 常见例子 | 空指针、除以零、文件找不到等 | 内存溢出、栈溢出等 |
二、异常与错误的继承关系(类层次结构)
理解它们的继承关系是掌握 Java 异常处理机制的关键。
php
java.lang.Throwable
├─ java.lang.Error(系统错误,不可处理)
│ ├─ OutOfMemoryError
│ ├─ StackOverflowError
│ ├─ NoClassDefFoundError
│ └─ ...(其他系统级错误)
└─ java.lang.Exception(程序异常,可处理)
├─ 检查型异常(必须处理)
│ ├─ IOException
│ │ ├─ FileNotFoundException
│ │ └─ ...
│ ├─ SQLException
│ ├─ ClassNotFoundException
│ └─ ...
└─ 非检查型异常(无需强制处理,继承自 RuntimeException)
├─ RuntimeException
│ ├─ NullPointerException
│ ├─ IndexOutOfBoundsException
│ │ ├─ ArrayIndexOutOfBoundsException
│ │ └─ StringIndexOutOfBoundsException
│ ├─ IllegalArgumentException
│ ├─ ClassCastException
│ └─ ...
└─ ...(其他非检查型异常,如 `UnsupportedOperationException`)
-
java.lang.Object:Java 中所有类的根父类。 -
java.lang.Throwable:这是所有错误和异常的顶层父类 。只有Throwable类型的对象才能被throw语句抛出,并且被catch语句捕获。它有两个重要的子类:-
java.lang.Error:用于表示严重的、无法处理的错误。 -
java.lang.Exception:用于表示可以被处理的异常。java.lang.RuntimeException:Exception的一个重要子类,所有非受检异常都继承自它。
-
三、异常的两大分类:受检和非受检
Java 里所有异常都继承自java.lang.Exception类,根据处理要求不同,分为两类,重点记清楚区别:
1. 受检异常(Checked Exception)
- 特点:编译时就会 "提醒" 你必须处理,不处理编译器不让通过
- 原因:大多是外部环境导致的,比如文件不存在、网络断开
- 常见例子:
IOException(文件读写异常)、SQLException(数据库操作异常) - 处理要求:要么用
try-catch捕获,要么用throws声明交给调用者处理
2. 非受检异常(Unchecked Exception)
- 特点:编译时不报错,运行时才可能出现
- 原因:大多是程序员的逻辑错误导致的,比如数组越界、用了空对象
- 常见例子:
NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组越界) - 处理要求:不用强制处理,但要在编码时避免(比如判断数组索引是否合法)
提示:所有非受检异常都继承自RuntimeException类,记住这个核心类就好~
四、异常处理核心:try-catch-finally 三板斧
这是 Java 处理异常的核心机制,就像给风险代码装了 "安全网",我们用计算器案例一步步拆解:
1. 原始问题代码(会崩溃)
ini
import java.util.Scanner;
public class SimpleCalculator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入第一个数字: ");
int num1 = scanner.nextInt();
System.out.print("请输入第二个数字: ");
int num2 = scanner.nextInt();
int result = num1 / num2; // 输入0就会崩溃
System.out.println("结果是: " + result);
scanner.close();
}
}
2. try 块:包裹风险代码
把可能出错的代码放进try里,让 Java "尝试" 执行:
csharp
try {
int result = num1 / num2; // 可能抛出异常的代码
System.out.println("结果是: " + result);
}
3. catch 块:捕获并处理异常
如果try里的代码真的出错,catch就会像安全网一样接住异常,执行备用逻辑:
csharp
catch (ArithmeticException e) {
// 只捕获"算术异常"(比如除以零)
System.out.println("【错误提示】");
System.out.println("出错原因:" + e.getMessage()); // 获取具体错误信息
System.out.println("除数不能为零,请重新输入!");
}
4. finally 块:必执行的收尾工作
不管有没有异常,finally里的代码都会执行,适合做资源清理(比如关文件、关连接):
csharp
finally {
System.out.println("\n--- 计算结束,资源已释放 ---");
scanner.close(); // 确保关闭输入流
}
完整健壮版代码(不崩溃)
csharp
import java.util.Scanner;
public class RobustCalculator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入第一个数字: ");
int num1 = scanner.nextInt();
System.out.print("请输入第二个数字: ");
int num2 = scanner.nextInt();
try {
int result = num1 / num2;
System.out.println("结果是: " + result);
} catch (ArithmeticException e) {
System.out.println("\n【错误提示】");
System.out.println("出错原因:" + e.getMessage());
System.out.println("除数不能为零,请重新输入!");
} finally {
System.out.println("\n--- 计算结束,资源已释放 ---");
scanner.close();
}
}
}
运行效果:输入 10 和 0 后,程序不会崩溃,而是友好提示错误,完美收尾~
五、进阶技巧:throw 和 throws 关键字
除了被动捕获异常,我们还能主动处理异常,这两个关键字要记牢:
1. throw:手动抛出异常
当检测到非法条件时,主动创建异常并抛出,比如判断除数为 0 时直接报错:
arduino
if (num2 == 0) {
// 手动抛出异常,自定义错误信息
throw new ArithmeticException("除数不能为零");
}
2. throws:声明异常,转移责任
如果一个方法不想处理异常,可以用throws声明,告诉调用者 "这里可能出错,你要处理":
csharp
// 声明该方法可能抛出算术异常
public static int divide(int num1, int num2) throws ArithmeticException {
if (num2 == 0) {
throw new ArithmeticException("除数不能为零");
}
return num1 / num2;
}
// 调用者必须处理异常
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("结果是: " + result);
} catch (ArithmeticException e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
六、高阶玩法:多异常捕获 + 自定义异常
掌握基础后,解锁两个实用高阶技巧,让异常处理更灵活精准:
1. 多异常捕获:一次处理多种错误
实际开发中,try块可能抛出多种异常(比如同时有算术异常、空指针异常),不用写多个独立catch,有两种高效处理方式:
方式 1:多异常并列捕获(Java 7+ 支持)
多个异常用竖线|分隔,适用于异常处理逻辑相同的场景:
ini
try {
String str = null;
int num1 = Integer.parseInt(str); // 可能抛NumberFormatException
int num2 = 0;
int result = num1 / num2; // 可能抛ArithmeticException
} catch (NumberFormatException | ArithmeticException e) {
// 同一逻辑处理两种异常
System.out.println("输入非法或计算错误:" + e.getMessage());
}
方式 2:分层捕获(父异常在后)
如果不同异常需要不同处理逻辑,按 "子类异常在前、父类异常在后" 的顺序捕获:
csharp
try {
String[] arr = {"10", "2", null};
int num1 = Integer.parseInt(arr[0]);
int num2 = Integer.parseInt(arr[2]); // 可能抛NullPointerException
int result = num1 / num2; // 可能抛ArithmeticException
} catch (NullPointerException e) {
// 专门处理空指针异常
System.out.println("数组元素为空,请检查输入:" + e.getMessage());
} catch (ArithmeticException e) {
// 专门处理算术异常
System.out.println("计算错误,除数不能为零:" + e.getMessage());
} catch (Exception e) {
// 父类异常兜底,捕获其他未预料的异常
System.out.println("未知错误:" + e.getMessage());
}
⚠️ 注意:不能把父异常(如Exception)放在子类异常前面,否则子类异常的catch块会永远执行不到,编译器会报错。
2. 自定义异常:贴合业务场景的错误提示
Java 内置异常只能覆盖通用场景,实际开发中需要贴合业务的异常(比如 "用户年龄不合法""余额不足"),这时可以自定义异常类:
自定义异常步骤:
- 继承
Exception(受检异常)或RuntimeException(非受检异常); - 提供无参构造和带错误信息的构造方法(方便传递异常原因)。
实战:自定义 "年龄不合法异常"
scala
// 自定义受检异常(继承Exception)
public class IllegalAgeException extends Exception {
// 无参构造
public IllegalAgeException() {}
// 带错误信息的构造方法
public IllegalAgeException(String message) {
super(message); // 调用父类构造,传递错误信息
}
}
// 自定义非受检异常(继承RuntimeException)
public class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
}
使用自定义异常:
csharp
public class UserService {
// 注册用户:年龄必须在18-60岁之间(受检异常,需throws声明)
public void register(String name, int age) throws IllegalAgeException {
if (age < 18 || age > 60) {
// 手动抛出自定义异常
throw new IllegalAgeException("用户" + name + "年龄不合法:" + age + "岁,需在18-60岁之间");
}
System.out.println("用户" + name + "注册成功!");
}
// 转账:余额不足时抛出非受检异常
public void transfer(String userName, double balance, double amount) {
if (amount > balance) {
throw new InsufficientBalanceException("用户" + userName + "余额不足:当前余额" + balance + ",转账金额" + amount);
}
System.out.println("转账成功!剩余余额:" + (balance - amount));
}
// 测试自定义异常
public static void main(String[] args) {
UserService service = new UserService();
// 处理受检异常(必须try-catch或throws)
try {
service.register("张三", 17);
} catch (IllegalAgeException e) {
System.out.println("注册失败:" + e.getMessage());
}
// 处理非受检异常(可选try-catch)
try {
service.transfer("李四", 100, 200);
} catch (InsufficientBalanceException e) {
System.out.println("转账失败:" + e.getMessage());
}
}
}
运行结果:
注册失败:用户张三年龄不合法:17岁,需在18-60岁之间
转账失败:用户李四余额不足:当前余额100.0,转账金额200.0
六、核心总结:异常处理的 3 个原则
- 能处理的异常一定要处理,别让程序粗暴崩溃;
- 用
try包裹风险代码,catch精准捕获(多异常按 "子类在前、父类在后" 顺序),finally清理资源; - 受检异常必须处理(
try-catch或throws),非受检异常尽量避免(靠逻辑判断),业务异常用自定义异常优化提示。
异常处理是 Java 程序健壮性的基石,刚开始不用追求复杂,先把try-catch-finally用熟,能解决除以零、空指针这些常见问题就够了。随着编程经验增加,再慢慢学习自定义异常、多异常捕获等进阶用法~