1. 异常作为 API 的一部分
为什么异常是方法“契约”的一部分?
当你编写一个方法时,不仅要定义参数与返回值,还要定义它可能抛出的异常。这属于你的方法与使用者之间的“契约”。如果方法可能抛出异常,所有调用它的人都应该知道——以便正确处理错误(或者至少在程序因错误而突然退出时不至于意外)。
示例:
public void readFile(String filename) throws IOException {
// ... 读取文件
}
这里明确指出该方法可能抛出 IOException。这就是对方法使用者的信号:“请准备好处理文件读取错误!”
记录异常:Javadoc 中的 @throws 标记
为了让其他开发者理解你的方法可能抛出哪些异常,请在 Javadoc 中使用 @throws(或 @exception)标记。
示例:
/**
* 读取文件内容。
*
* @param filename 文件名
* @return 文件内容的字符串
* @throws IOException 如果发生文件读取错误
*/
public String readFile(String filename) throws IOException {
// ...
}
为何需要这样做?
- 帮助其他开发者了解需要处理哪些错误。
- IDE 和文档生成器(例如 Javadoc)会在提示中直接显示这些异常。
- 提升代码的可靠性与可预测性。
API 中的 checked 与 unchecked 异常
Checked 异常(继承自 Exception,但不包括 RuntimeException)属于方法契约的一部分。调用方必须要么处理,要么显式继续抛出(throws)。
Unchecked 异常(RuntimeException 及其子类)通常表示编程错误(例如 NullPointerException、IllegalArgumentException)。它们不必在方法签名中声明,但如果你的方法可能抛出此类错误(例如参数不合法),也应在 Javadoc 中予以说明。
示例:
/**
* 将 a 除以 b。
* @param a 被除数
* @param b 除数
* @return 除法结果
* @throws IllegalArgumentException 如果 b == 0
*/
public int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("除数不能为零");
return a / b;
}
异常与 API 设计
- 站在方法使用者的角度思考:哪些错误他可以、也应该处理?哪些是 bug,哪些是“业务中的正常情况”?
- 不要滥用 checked 异常:如果错误本质上是 bug(例如参数不合法),更适合抛出 unchecked 异常。
- 记录所有可能“向上”抛出的异常。
2. try-with-resources 构造
问题:如何安全地关闭资源?
在很多任务中需要处理使用后必须关闭的资源:文件、网络连接、数据库等。如果忘记关闭资源,可能导致内存泄漏、文件被锁定或其他问题。
以前:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
// ...
} catch (IOException e) {
// 错误处理
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// 关闭时的错误处理
}
}
}
代码很多,容易出错,也可能忘记关闭资源。
解决方案:try-with-resources
从 Java 7 开始引入了 try-with-resources 构造,它会在块内发生异常时也自动关闭所有资源。
语法:
try (ResourceType resource = new ResourceType(...)) {
// 使用资源
} catch (ExceptionType e) {
// 错误处理
}
示例:
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("读取文件时出错: " + e.getMessage());
}
// reader.close() 会自动被调用!
它是如何工作的?
- 在 try 后的括号中声明需要关闭的资源。
- 离开 try 块后(即使发生异常!)会为每个资源调用 close() 方法。
- 这只对实现了 AutoCloseable(或其父接口 Closeable)的资源有效。
AutoCloseable 接口:
public interface AutoCloseable {
void close() throws Exception;
}
Java 的所有标准资源(文件、流、数据库连接)都实现了该接口。
可以声明多个资源
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
即使发生异常,两个资源也都会被自动关闭。
try-with-resources 的优势
- 安全:即使出错,资源也总会被关闭。
- 简洁:更少的代码,降低出错概率。
- 可读:一眼就能看出使用了哪些资源以及它们何时关闭。
3. 实践:用 try-with-resources 编写安全代码
示例:读取文件
public static void printFirstLine(String filename) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line = reader.readLine();
System.out.println("第一行: " + line);
} catch (IOException e) {
System.out.println("错误: " + e.getMessage());
}
}
示例:写入文件
public static void writeToFile(String filename, String text) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
writer.write(text);
} catch (IOException e) {
System.out.println("写入时出错: " + e.getMessage());
}
}
示例:自定义资源
如果你编写的类需要关闭,只需实现 AutoCloseable:
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("资源已关闭!");
}
}
现在可以在 try-with-resources 中使用它:
try (MyResource res = new MyResource()) {
// 使用资源
}
4. 常见错误与最佳实践
错误 1:忘记关闭资源(未使用 try-with-resources)。
如果不使用 try-with-resources,很容易忘记关闭文件或流——这会导致资源泄漏。
错误 2:试图在未实现 AutoCloseable 的对象上使用 try-with-resources。
如果你的类未实现该接口,编译器将不允许在 try-with-resources 中使用它。
错误 3:没有在 API 中记录异常。
如果你的方法可能抛出异常——请务必在签名中声明(throws),并在 Javadoc 中使用 @throws 说明。这将帮助他人正确使用你的代码。
错误 4:捕获 Exception 而不是具体的异常类型。
只捕获那些你确实预期并且能够处理的异常。
GO TO FULL VERSION