第11章:异常处理 —— 当程序生病了
本章语言侧重:Java ⭐⭐⭐ | Python 和 C++ 的对比在末尾统一给出。
一个故事开头
小王花了三天写了一个奶茶店点单系统,今天第一天上线。
第一位顾客走到柜台前:"你好,我要一杯陨石拿铁。"
小王自信满满地在键盘上输入 "陨石拿铁"——菜单里没有这个饮品。
程序崩溃了。
黑色的终端窗口吐出一串红色的字,像是在嘲笑他。顾客站在柜台前,盯着一脸尴尬的小王,场面一度非常尴尬。
小王盯着屏幕上那一大坨红色文字,心想:"为什么我的程序不能好好地说一声'没有这个商品',然后继续跑?"
这就是本章要解决的问题。
1. 你一定见过这个
在正式开始之前,我们先回忆一下你已经见过的场面。
编译错 vs 运行错
你的代码会遇到的错误分两种:
编译错(Compile-time Error):程序还没跑就被编译器拦下来了——像你还没出门就被妈妈拽回来说"鞋穿反了"。
System.out.println("Hello" // ❌ 少写了右括号,编译不通过
// 编译器会温柔(其实并不温柔)地告诉你:missing ')'运行错(Runtime Error):编译是过了,跑起来才炸——像你出了门走两步踩到香蕉皮一样。
int[] menu = new int[5];
System.out.println(menu[99]); // 明明只有5个菜品,你非要第100号
// 编译通过 ✓ 跑起来就炸了 ✗💥 拆了它:先炸几次过过瘾
创建一个 Crasher.java,一行一行地试:
public class Crasher {
public static void main(String[] args) {
// 场景1:数学老师的噩梦
int result = 10 / 0; // ← 猜猜会怎样?
System.out.println("这行永远不会执行");
}
}运行它。你看到的红色文字就是 ArithmeticException——Java 告诉你:"除以0这种事,我做不到。"
现在把除以0那行注释掉,换成下面这几个,挨个跑一遍:
// 场景2:数错数组长度
int[] scores = {85, 90, 78};
System.out.println(scores[100]);
// 场景3:那东西根本不存在
String name = null;
System.out.println(name.length());
// 场景4:牛奶不能当电话号码
String phone = "奶茶";
int num = Integer.parseInt(phone);你看到了什么? 每次 Java 都给了你一个大大的红色错误信息。它的格式长这样:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Crasher.main(Crasher.java:4)这个红色文字叫异常信息(Exception Message)。它告诉你三样东西:
- 什么异常——
ArithmeticException(数学异常) - 为什么——
/ by zero(除以0造成的) - 在哪——
Crasher.main(Crasher.java:4)(在 Crasher.java 的第四行)
现在把 result = 10 / 0 改写成:
int result = 10 / 3; // 不会报错,结果是 3 (整数除法)10/3 可以但 10/0 不行?在计算机世界里,"除以0"没有定义——不是"接近多少"的问题,是真的没有答案。Java 很诚实:做不了就报错,不糊弄你。
2. 异常的比喻——程序感冒了
你见过上面那些"炸了"的场景了。现在正式介绍一下异常是什么。
想象一个人着凉了:
- 他身体会发出信号——打喷嚏、发烧、流鼻涕
- 这个信号告诉你:"我不舒服,需要处理"
程序也一样:
- 当它遇到自己处理不了的情况(除以0、数组越界、空指针),它会抛出一个异常对象(throw an exception)
- 这个异常对象就像是程序的"病历单",上面写着:
- 什么病(异常类型——比如
ArithmeticException) - 在哪感染了(代码的位置——比如
Crasher.java 第4行) - 病因是什么(错误信息——比如
/ by zero)
- 什么病(异常类型——比如
// Java 内部大概是这样做的
if (denominator == 0) {
throw new ArithmeticException("/ by zero"); // ← 抛出异常
}异常家族谱
Java 有一张异常家族关系图,你没见过但肯定猜得到长什么样——因为 Java 里几乎所有东西都是类,异常也是一堆类。
Object
└── Throwable ← 所有异常的祖宗("可以抛的东西")
├── Error ← 系统级别的严重问题(内存用完了)
└── Exception ← 程序级别的错误(我们关心的)
├── RuntimeException ← 运行时异常(写代码不小心造成的)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ArithmeticException
│ └── NumberFormatException
└── IOException 等 ← 编译时异常(外面的因素造成的)
└── FileNotFoundException你不需要背这个图。 但你要知道两件事:
RuntimeException及其子类——你写代码不小心造成的(比如数组越界、空指针)Exception的其他子类——外部环境造成的(比如文件不存在、网络断了)
Java 对这两种区别对待,我们后面会讲。
💥 拆了它:看看最出名的空指针
空指针(NullPointerException)——所有 Java 程序员见得最多的异常,没有之一。
public class NullDemo {
public static void main(String[] args) {
String name = null; // "null" 是 Java 里的"啥也没有"
// 你现在试图对一个"啥也没有"的东西调用方法
System.out.println(name.length());
}
}运行结果:
Exception in thread "main" java.lang.NullPointerException
at NullDemo.main(NullDemo.java:5)null 就是"不指向任何对象"。你把引用变量想成墙上的钥匙钩——null 就是钩子上空空如也。对着空钩子喊"拿钥匙开门",它也很无奈。
// 更形象的例子
public class NullFriend {
static class Friend {
String name;
void sayHi() {
System.out.println("你好,我是" + name);
}
}
public static void main(String[] args) {
Friend f = null; // 你有一个"朋友"的名额,但还没交到朋友
f.sayHi(); // ❌ 对一个不存在的人说"你好"
// 结果:NullPointerException
}
}实际场景:
// 你在数据库里查用户,结果没查到
User user = findUserById(999); // 返回 null
System.out.println(user.getName()); // ❌ NPE!🧪 动手试试
写一个程序,故意触发以下三种异常并观察它们的报错信息:
- 一个
NullPointerException——任意方式 - 一个
ArrayIndexOutOfBoundsException - 一个你之前没见到的异常(上网搜或自己猜一个)
每次看看报错信息告诉你什么。
3. try-catch:给程序穿上防弹衣
小王看着屏幕上红色的报错,心想:"有没有办法让程序出错了也不崩溃,而是优雅地说一声'出错了'然后继续跑?"
有。这就是 try-catch。
最简单的用法
public class SafeDivider {
public static void main(String[] args) {
try {
// try 块:放"可能出问题"的代码
int result = 10 / 0;
System.out.println("结果:" + result); // 这行不会执行
} catch (Exception e) {
// catch 块:出问题后执行的代码
System.out.println("哎呀,算错了!");
}
System.out.println("程序还在正常运行!");
}
}运行结果:
哎呀,算错了!
程序还在正常运行!发生了什么?
- Java 执行
try块里的代码 - 遇到
10 / 0,发现有问题 - 不再执行 try 块里剩余的行(
System.out.println("结果..."没跑) - 跳到
catch块,执行里面的代码 - 执行完 catch,继续往下走
注意:try-catch 不是让错误消失,是让程序从崩溃中挺过来,继续往下走。
抓具体的异常
上面写的 catch (Exception e) 就像"不管什么病都挂急诊"——能解决问题但不够精确。更好的做法是抓具体的异常类型:
public class SpecificCatch {
public static void main(String[] args) {
try {
int[] scores = {85, 90, 78};
System.out.println(scores[5]); // 越界了
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数错了!数组只有 " + scores.length + " 个元素");
}
}
}为什么指定具体类型?假设你的 try 块里既有数组越界又有除以0,catch (Exception e) 会把两种错误混一起处理——你分不清是哪出了事。
public class TooBroadCatch {
public static void main(String[] args) {
int[] scores = {85, 90};
try {
// 到底哪个出错了?
scores[99] = 100; // 数组越界
int x = 100 / 0; // 除以0
} catch (Exception e) {
// ❌ 不管哪个错,输出的都一样
System.out.println("出了点问题:" + e.getMessage());
}
}
}💥 拆了它:看看 e 到底是什么
在你的 catch 块里,把 e 打印出来,看看 Java 给了你什么:
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println(e); // 打印异常对象
System.out.println(e.getMessage()); // 只打原因
e.printStackTrace(); // 打印完整堆栈——最常用!
}运行结果:
java.lang.ArithmeticException: / by zero
/ by zero
java.lang.ArithmeticException: / by zero
at Main.main(Main.java:4)e.printStackTrace() 会打出错误从最里层到最外层的完整调用链——调试时靠它救命。
🧪 动手试试
写一个程序,让它在同一个 try 块里可能触发两种不同的异常(比如数组越界和空指针),用两个 catch 分别捕获并打印不同的提示信息。
4. 多级 catch:区分不同的"病"
语法:多个 catch 一块走
public class MultiCatch {
public static void main(String[] args) {
try {
String input = args[0]; // 可能 ArrayIndexOutOfBounds(没传参数)
int num = Integer.parseInt(input); // 可能 NumberFormatException
int result = 100 / num; // 可能 ArithmeticException(传了 0)
System.out.println("结果是:" + result);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("你没传参数!运行方式:java MultiCatch <数字>");
} catch (NumberFormatException e) {
System.out.println("传的不是数字!请输入整数");
} catch (ArithmeticException e) {
System.out.println("不能除以0!请输入非零数字");
}
}
}试试以下三种运行方式:
java MultiCatch # → 你没传参数!
java MultiCatch 奶茶 # → 传的不是数字!请输入整数
java MultiCatch 0 # → 不能除以0!请输入非零数字Java 从上到下找第一个能匹配的 catch——像医院分诊,你说"发烧",她就指你去发热门诊,不会让你在急诊排半天。
⚠️ 顺序很重要:子类在前,父类在后
// ❌ 错误顺序——父类会"拦截"子类
try {
String input = null;
System.out.println(input.length());
} catch (Exception e) { // Exception 是 RuntimeException 的父类
System.out.println("万能catch");
} catch (NullPointerException e) { // ❌ 编译错误:永远到不了这里
System.out.println("空指针!");
}原因:catch (Exception e) 已经覆盖了所有异常,后面的 catch 永远到不了。Java 编译器发现这个逻辑问题,直接拒绝编译。
// ✅ 正确顺序——子类在前
try {
String input = null;
System.out.println(input.length());
} catch (NullPointerException e) {
System.out.println("空指针!细粒度处理");
} catch (Exception e) {
System.out.println("其他异常:兜底处理");
}💥 拆了它:试试反过来会怎样
把上面"错误顺序"的代码编译一下,看看 Java 编译器报的编译错误是什么。记住:Java 会在你跑之前就发现这种逻辑问题,不让你编译通过。
Java 7+ 的简化写法:catch 管道
// 如果你想对多种异常做同样的处理
try {
String input = args[0];
int num = Integer.parseInt(input);
} catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
// 用 | 分隔多个异常类型
System.out.println("参数有问题:要么没传,要么不是数字");
}省事,不用给两种异常各写一遍同样的 catch。
5. finally:不管怎样都要做的事
为什么需要 finally?
想象一下:你去奶茶店,店员开始做你的奶茶了——突然发现牛奶用完了。店员告诉你"做不了",然后呢?
你觉得店员会不会把刚才已经拿出来的杯子、配料随便扔在台面上就回家了?不会。她应该把台面收拾干净。
程序也一样。有些操作不管出没出错都必须做完——比如关闭文件、释放连接、解锁资源。
public class FinallyDemo {
public static void main(String[] args) {
try {
System.out.println("1. 开始做奶茶");
int result = 10 / 0; // 炸了
System.out.println("2. 奶茶做好了(这条不会执行)");
} catch (Exception e) {
System.out.println("3. 出错了!牛奶用完了");
} finally {
// finally 块:不管 try 块有没有异常、catch 有没有执行——它都会执行
System.out.println("4. 💪 收拾台面!关店门!");
}
System.out.println("5. 程序结束");
}
}运行结果:
1. 开始做奶茶
3. 出错了!牛奶用完了
4. 💪 收拾台面!关店门!
5. 程序结束💥 拆了它:如果 try 里没有异常呢?
把上面的 int result = 10 / 0 改成 int result = 10 / 2,看看 finally 还会不会执行。
1. 开始做奶茶
2. 奶茶做好了
4. 💪 收拾台面!关店门!
5. 程序结束结果:finally 仍然执行了。 这就是 finally 的特点——无论 try 正常结束还是异常结束,它都会跑。
再拆一次:如果 catch 里又出错了呢?
public class FinallyAlwaysRuns {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println("catch 块");
int x = 10 / 0; // catch 里又除以0!
} finally {
// ✅ finally 仍然会执行!神奇吧?
System.out.println("finally 块——就算 catch 也炸了,我还是要执行");
}
}
}结论:finally 是 Java 里最倔的东西——想尽办法也要跑完。
finally 的典型用途
import java.io.*;
public class FinallyFileDemo {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("menu.txt"); // ← 可能 FileNotFoundException
// ... 读取文件内容
} catch (FileNotFoundException e) {
System.out.println("菜单文件不存在");
} finally {
// 不管文件读没读到,都要释放资源
if (fis != null) {
try {
fis.close(); // close() 本身也可能抛异常...
} catch (IOException e) {
System.out.println("关闭文件时出了问题");
}
}
}
}
}注意上面的 finally 块里面还套了一层 try-catch——因为 fis.close() 自己也可能抛异常。这就是 Java 7 之前的"经典"写法,又臭又长。后面我们来看怎么改善。
6. throw:主动说"我做不了"
什么时候需要手动扔异常?
上面所有的例子都是 Java 自动帮你 throw 的——10 / 0 自动抛 ArithmeticException,越界自动抛 ArrayIndexOutOfBoundsException。
但有时候你需要自己决定什么情况是"异常"。
public class MilkTeaShop {
public static void main(String[] args) {
order("芒果波波", -5); // -5 杯?你认真的?
}
static void order(String drink, int quantity) {
if (quantity <= 0) {
// 你自己决定:数量<=0 是异常情况
throw new IllegalArgumentException("数量必须大于0,但你传了 " + quantity);
}
System.out.println("订单已接收:" + drink + " × " + quantity);
}
}运行结果:
Exception in thread "main" java.lang.IllegalArgumentException: 数量必须大于0,但你传了 -5
at MilkTeaShop.order(MilkTeaShop.java:9)
at MilkTeaShop.main(MilkTeaShop.java:4)throw 的作用:你说"这里有问题,我不处理了,让上层去管"。
实际场景:检查参数
public class BankAccount {
private double balance; // 余额
void withdraw(double amount) {
if (amount <= 0) {
// ❌ 取款金额不能是负数或零
throw new IllegalArgumentException("取款金额必须为正数");
}
if (amount > balance) {
// ❌ 余额不足
throw new RuntimeException("余额不足!当前余额:" + balance + ",想取:" + amount);
}
balance -= amount;
System.out.println("取款成功,剩余余额:" + balance);
}
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.balance = 100;
account.withdraw(200); // 只有100,取200?
// → Exception: 余额不足!
System.out.println("这条还会执行吗?"); // ❌ 不会,程序已经炸了
}
}💥 拆了它:自己加的 throw 用 try-catch 接住
public class CatchOwnThrow {
static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能是负数吧老铁");
}
System.out.println("年龄验证通过:" + age);
}
public static void main(String[] args) {
try {
checkAge(-5); // 你主动扔的异常
} catch (IllegalArgumentException e) {
System.out.println("抓到了:" + e.getMessage());
}
System.out.println("程序还在跑!");
}
}运行结果:
抓到了:年龄不能是负数吧老铁
程序还在跑!关键:throw 扔出来的异常和 Java 自动扔的没有本质区别——可以用 try-catch 接住,也可以让它一路炸到 JVM 那层(然后程序崩溃)。
7. throws:自己不处理,甩锅给上层
throws 是啥
throw 是你亲手扔异常。throws 是你声明"我这个方法可能会出这个错,调用我的人你看着办"。
public class ThrowsDemo {
// 这个方法声明了:我可能会抛异常,不管了,交给你处理
static void readFile(String path) throws FileNotFoundException {
// FileNotFoundException 是"检查型异常"——必须声明或处理
FileInputStream fis = new FileInputStream(path);
// 如果文件不存在,new FileInputStream 会抛 FileNotFoundException
}
public static void main(String[] args) {
// ❌ 编译错误!因为 readFile 声明了 throws
// 调用者必须处理这个异常
readFile("not_exists.txt");
}
}两种类型的异常
还记得之前的异常家族树吗?
Exception
├── RuntimeException 及其子类 ← 非检查异常(Unchecked)
└── 其他 Exception ← 检查异常(Checked)Java 对它们的区别对待非常直接:
| 异常类型 | 必须处理吗? | 例子 |
|---|---|---|
RuntimeException(运行时异常) | 不用强制处理 | 空指针、数组越界、除以0 |
Checked Exception(检查型异常) | 必须处理——不处理编译不通过 | 文件找不到、网络超时 |
为什么要有这种区别?Java 设计者的想法是:
NullPointerException是你写代码不小心造成的——编译器假设你会好好写代码,不用强制检查FileNotFoundException是运行时环境决定的——文件可不存在,这是你无法控制的——编译器强制你处理这种情况
// ✅ 非检查异常(RuntimeException)——不需要 throws,也不需要 catch
void riskyMethod() {
int x = 10 / 0; // ArithmeticException 是 RuntimeException
// 编译器不会强制你处理
}
// ❌ 检查型异常——编译器强制你处理
void readMenu() throws FileNotFoundException {
// ...
}
// 或者
void readMenu() {
try {
// ...
} catch (FileNotFoundException e) {
// ...
}
}现实中的感受
很多 Java 开发者觉得检查型异常烦人——特别是就想快速写个小程序的时候。但它也有好处:你刚接手一个新项目,看到方法签名上写着 throws IOException,马上就知道这方法可能要读写文件,心里有个底。
💥 拆了它:看看检查异常编译不通过的样子
import java.io.*;
public class CheckedDemo {
public static void main(String[] args) {
// 直接调用可能抛检查异常的方法,但不处理
Thread.sleep(1000); // ← 编译错误!InterruptedException 是检查型异常
}
}试一下,看看编译器说什么。然后改成:
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000); // ✅ 改成 throws 声明,把责任甩给调用者
}或者:
public static void main(String[] args) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被中断了:" + e.getMessage());
}
}拿个说法帮你记:检查型异常就是"签了字的免责声明"——方法签名上白纸黑字说了"我可能出这问题",调用者没法装没看见。
8. 自定义异常:给错误起个有意义的名字
为什么要自定义异常?
// 在你的奶茶店点单系统里
throw new RuntimeException("原料不足");
throw new RuntimeException("余额不够");
throw new RuntimeException("商品不存在");三个不同的错误,全都叫 RuntimeException。你的 catch 块区分不了它们——除非去检查那个字符串:
} catch (RuntimeException e) {
if (e.getMessage().equals("原料不足")) {
// ...
} else if (e.getMessage().equals("余额不够")) {
// ...
}
// 这写法太蠢了
}更好的方式:给你的每种错误起一个有名字的异常类。
怎么定义
超简单——就是写一个类,继承 Exception(或 RuntimeException):
// 继承 RuntimeException = 非检查异常
class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String itemName, int requested, int available) {
super("库存不足:" + itemName + " 需求" + requested + ",库存仅" + available);
}
}
class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(double balance, double required) {
super("余额不足:当前 " + balance + " 元,需要 " + required + " 元");
}
}
class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String productName) {
super("商品不存在:" + productName);
}
}在项目中使用
public class MilkTeaOrderSystem {
static class Product {
String name;
double price;
int stock;
Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
}
static class Customer {
String name;
double balance;
Customer(String name, double balance) {
this.name = name;
this.balance = balance;
}
}
static void order(Customer customer, Product product, int quantity) {
if (product.stock < quantity) {
throw new InsufficientStockException(product.name, quantity, product.stock);
}
double total = product.price * quantity;
if (customer.balance < total) {
throw new InsufficientBalanceException(customer.balance, total);
}
customer.balance -= total;
product.stock -= quantity;
System.out.println("✅ 下单成功:" + product.name + " × " + quantity);
System.out.println(" 花费 " + total + " 元,剩余余额 " + customer.balance);
}
public static void main(String[] args) {
Product 波波奶茶 = new Product("波波奶茶", 15.0, 10);
Customer 小明 = new Customer("小明", 30.0);
try {
order(小明, 波波奶茶, 1); // ✅ 正常
order(小明, 波波奶茶, 100); // ❌ 库存不足
} catch (InsufficientStockException e) {
System.out.println("库存问题:" + e.getMessage());
} catch (InsufficientBalanceException e) {
System.out.println("余额问题:" + e.getMessage());
}
System.out.println("\n程序正常结束!");
}
}💥 拆了它:把自定义异常换父类
把上面两个自定义异常改成继承 Exception(检查型异常)而不是 RuntimeException——看看会发生什么。编译要怎么改?
发现没有?如果继承 Exception(检查型),order 方法签名得加 throws,调用处也得加 try-catch。这就是"检查 vs 非检查"在实战中的区别。
🧪 动手试试
为你的奶茶店系统再加一个自定义异常:PaymentTimeoutException——当支付过程超过30秒时抛出。想一想这个异常应该继承 RuntimeException 还是 Exception?
9. try-with-resources:自动关门的帮手
回忆刚才的脏活
还记得我们关闭文件时要写多少代码吗?
// Java 7 之前的"经典"写法
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 读文件...
} catch (IOException e) {
System.out.println("出错了");
} finally {
if (fis != null) {
try {
fis.close(); // close 本身也可能抛异常!
} catch (IOException e) {
// 有什么办法呢,再抓一次呗
}
}
}这段代码里,真正做事的逻辑只有两三行,剩下七八行全是在处理"收尾"。又丑又容易写错(很多人忘了在 finally 里 close)。
Java 7 的救星
import java.io.*;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// try 后面的括号里放"资源"——资源必须实现 AutoCloseable 接口
try (FileInputStream fis = new FileInputStream("menu.txt")) {
// 读文件...
System.out.println("文件打开成功");
} catch (FileNotFoundException e) {
System.out.println("菜单文件不存在");
} catch (IOException e) {
System.out.println("读取文件出错");
}
// 不需要 finally 去 close——try-with-resources 会自动关闭
}
}try 块结束(不管是正常结束还是异常结束),Java 自动调用 fis.close()。不用写 finally,不用自己 catch close 的异常。
同一个道理适用于其他资源
import java.sql.*;
public class ResourceDemo {
public static void main(String[] args) {
// 多个资源用分号分隔
try (Connection conn = DriverManager.getConnection("jdbc:...");
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM menu");
// ...
} catch (SQLException e) {
System.out.println("数据库出错:" + e.getMessage());
}
// conn 和 stmt 都会自动关闭——而且按创建顺序的反向关闭
}
}💥 拆了它:验证资源确实被关闭了
写一个自己的类,实现 AutoCloseable 接口,在 close() 方法里打印一句"我被关了":
class MyResource implements AutoCloseable {
String name;
MyResource(String name) {
this.name = name;
}
public void close() {
System.out.println(name + " 被关闭了");
}
}
public class TestAutoClose {
public static void main(String[] args) {
try (MyResource r1 = new MyResource("资源1");
MyResource r2 = new MyResource("资源2")) {
System.out.println("正在使用资源...");
}
// 猜猜输出顺序?
}
}10. 易错点与最佳实践
❌ 错误1:吞掉异常
try {
int result = 10 / 0;
} catch (Exception e) {
// catch 块是空的!——异常被"吃掉了"
// 程序不会崩溃,但你也完全不知道出了什么事
// 这叫"吞异常"——最糟糕的写法之一
}问题在于你压住了错误信息,但错误本身还在。后续代码拿着错误的结果继续跑,跑出更离奇的 bug——而且几乎没法调试,因为原始信息被你吃了。
✅ 正确的做法:至少打一行日志
try {
int result = 10 / 0;
} catch (Exception e) {
System.err.println("除法出错:" + e.getMessage());
// 或者:e.printStackTrace();
// 或者用日志框架
}❌ 错误2:用异常控制程序流程
// ❌ 非常不推荐!
try {
int num = Integer.parseInt(input);
// 如果解析成功,说明是数字
} catch (NumberFormatException e) {
// 如果解析失败,说明不是数字
System.out.println("这不是一个数字");
}不推荐是因为异常机制比普通 if-else 代价大得多——要构造异常对象、填充堆栈。而且代码可读性也差。
✅ 更好的做法:
// 先检查,再做
if (input.matches("-?\\d+")) { // 提前检查是不是数字
int num = Integer.parseInt(input);
} else {
System.out.println("这不是一个数字");
}用异常做控制流,好比为了喊人出来直接拿锤子砸墙——功能上可行,但不该这么干。
❌ 错误3:在 finally 里 return
public static int test() {
try {
return 1; // 准备返回1
} finally {
return 2; // ❌ finally 里的 return 覆盖了 try 里的 return
}
}
// 调用 test() 返回的是 2,不是 1!finally 里别写 return——它会让 try 里的 return 原地失效。
✅ 最佳实践汇总
| 该做的 | 不该做的 |
|---|---|
| 抓具体的异常类型 | 只抓 Exception 或 Throwable |
| catch 块里记录错误信息 | 空的 catch 块 |
| 自定义异常时用有意义的类名和消息 | 到处 throw new RuntimeException("xxx") |
| 关闭资源用 try-with-resources | 手动在 finally 里 close |
| 异常仅用于真正的异常情况 | 用异常做正常的流程控制 |
| 保持 catch 简洁——只做必要的错误处理 | catch 里写几百行"处理逻辑" |
11. 三语言对照
异常机制对比
| 特性 | Java | Python | C++ |
|---|---|---|---|
| 语法 | try-catch-finally | try-except-finally | try-catch |
| 检查型异常 | ✅ 有 | ❌ 没有(全是非检查) | ❌ 没有 |
| 多异常捕获 | 多个 catch 或 `\ | ` 管道 | except (Type1, Type2) |
| 自定义异常 | 继承 Exception/RuntimeException | 继承 Exception | 继承 std::exception |
| 资源自动管理 | try-with-resources | with 语句 | RAII(析构函数) |
| finally | ✅ 有 | ✅ 有 | ❌ 没有(用 RAII 替代) |
Python 示例
# Python 的异常处理
class InsufficientStockError(Exception):
pass
class MilkTeaShop:
stock = {"波波奶茶": 10}
def order(self, name, quantity):
if name not in self.stock:
raise ValueError(f"商品不存在:{name}")
if self.stock[name] < quantity:
raise InsufficientStockError(f"库存不足")
self.stock[name] -= quantity
return f"下单成功:{name} × {quantity}"
try:
shop = MilkTeaShop()
result = shop.order("波波奶茶", 100)
except InsufficientStockError as e:
print(f"库存问题:{e}")
except ValueError as e:
print(f"参数问题:{e}")C++ 示例
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// 自定义异常
class InsufficientStockError : public runtime_error {
public:
InsufficientStockError(const string& msg) : runtime_error(msg) {}
};
class MilkTeaShop {
private:
int stock = 10;
public:
void order(int quantity) {
if (stock < quantity) {
throw InsufficientStockError("库存不足——需求" +
to_string(quantity) + ",库存" + to_string(stock));
}
stock -= quantity;
cout << "下单成功 × " + to_string(quantity) << endl;
}
};
int main() {
MilkTeaShop shop;
try {
shop.order(100);
} catch (const InsufficientStockError& e) {
cout << "库存问题:" << e.what() << endl;
} catch (const exception& e) {
cout << "其他错误:" << e.what() << endl;
}
return 0;
}(C++ 里没有 finally——因为 RAII(资源获取即初始化)通过对象的析构函数自动释放资源,设计上比 finally 更利落)
本章小结
从奶茶店老板的崩溃经历出发,我们走过了异常处理的整条路:
- 异常是什么 —— 程序感冒发烧时抛出的"病历单"
- try-catch —— 给程序穿上防弹衣,不让它崩溃
- 多级 catch —— 不同的病,不同的处理方法
- finally —— 不管怎样都要收拾台面
- throw —— 主动说"我干不了"
- throws —— 甩锅给上层
- 检查型 vs 非检查 —— 强制你考虑的 vs 信任你自己的
- 自定义异常 —— 给你的错误起个名字
- try-with-resources —— Java 自动帮你关门
下一章:泛型——把"类型"也变成参数,让你的代码更通用、更安全。
✅ 验收标准
完成本章后,你应该能:
- [ ] 用 try-catch 捕获并处理异常
- [ ] 区分 checked 和 unchecked 异常
- [ ] 用 finally 做清理操作
- [ ] 用 try-with-resources 自动关闭资源
- [ ] 用 throws 声明方法可能抛出的异常
📌 常见卡点
- 异常类型不匹配——捕获的是 Exception 但抛的是更具体的子类
- finally 里的 return 会覆盖 try 里的 return——别在 finally 里 return
- 吞掉了异常但没做任何处理——至少打日志
- try-with-resources 的资源类必须实现 AutoCloseable
🔜 现在不需要理解
- 自定义异常类的设计规范
- 异常的性能开销——只在真正异常时抛
- 断言——不是异常处理
- 全局异常处理器——框架层面的事
🧪 练习
基础:写一个除法计算器,用户从键盘输入两个数字,程序输出结果。如果用户输入的不是数字或除以0,捕获异常并提示重新输入。
进阶:用自定义异常改造第7章或第8章的学生成绩管理系统——当设置的成绩不在0-100范围时抛
InvalidScoreException,当学生列表已满时抛ClassFullException。挑战:实现一个简单的银行 ATM 模拟器。定义
Account类(余额、账户名),支持deposit()和withdraw()方法。自定义以下异常:InsufficientBalanceException(余额不足)、InvalidAmountException(金额≤0)、AccountNotFoundException(账号不存在)。然后写几个场景测试你的异常处理。