异常处理
checked vs unchecked、try-with-resources、异常链、自定义异常
什么时候用异常
异常用于表达"非预期但需要被处理"的情况,典型场景:读文件失败、网络断开、数据库连接超时、用户输入非法、JSON 解析错误。Java 把它们设计成可抛出/可捕获的对象,而不是返回值,调用方可以选择就地处理、向上传递、或者整体兜底。
- ✅ 合适场景:IO 失败 / 网络错误 / 资源不可用 / 业务校验不通过 / 配置缺失
- ❌ 不合适场景:用异常控制流程(比如用 NumberFormatException 判断字符串是不是数字)
- 判断原则:如果"出错"是常态的一部分(如 form 校验),考虑用返回值 / Optional;只有真正"异常"才抛
throw 与 throws
throw 用于在方法内抛出异常对象;throws 写在方法签名上,声明这个方法可能抛出哪些 checked 异常,强制调用者要么 catch、要么继续 throws。
// ThrowDemo.java
public class ThrowDemo {
static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("divide by zero");
}
return a / b;
}
public static void main(String[] args) {
System.out.println(divide(10, 2));
try {
System.out.println(divide(10, 0));
} catch (ArithmeticException e) {
System.out.println("caught: " + e.getMessage());
}
}
}checked vs unchecked
Java 把异常分两类:checked(受检)必须被处理或声明 throws,编译器强制;unchecked(RuntimeException 及其子类、Error)不强制处理。
- checked:IOException、SQLException、ClassNotFoundException 等——常见于库 API,强制使用者面对
- unchecked(RuntimeException 家族):NullPointerException、IllegalArgumentException、IllegalStateException、IndexOutOfBoundsException——多半是"程序员错误"
- Error:OutOfMemoryError、StackOverflowError——JVM 级别故障,不要 catch
// CheckedVsUnchecked.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CheckedVsUnchecked {
// 必须 throws 或就地处理(checked)
static String readConfig(String path) throws IOException {
return Files.readString(Path.of(path));
}
// 不需要声明(unchecked)
static int parseAge(String s) {
int n = Integer.parseInt(s); // 解析失败抛 NumberFormatException
if (n < 0) throw new IllegalArgumentException("age < 0");
return n;
}
public static void main(String[] args) throws IOException {
System.out.println(parseAge("30"));
try {
parseAge("abc");
} catch (NumberFormatException e) {
System.out.println("bad number: " + e.getMessage());
}
}
}try-catch-finally 与多重 catch
finally 块无论是否抛异常都会执行,常用于清理资源(旧版做法)。多重 catch 用 `|` 合并:多种异常做相同处理。
// TryCatch.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
public class TryCatch {
public static void main(String[] args) {
try {
String content = Files.readString(Path.of("/no/such/file"));
System.out.println(content);
} catch (NoSuchFileException e) {
System.out.println("file missing: " + e.getFile());
} catch (IOException | RuntimeException e) {
// 多种异常合并处理
System.out.println("other error: " + e);
} finally {
System.out.println("cleanup runs anyway");
}
}
}try-with-resources(IO / DB 必备)
凡是"打开了就必须关闭"的资源——文件流、数据库连接、HTTP client、socket——都应该用 try-with-resources。括号里声明的对象只要实现了 AutoCloseable,离开 try 块时自动 close(),比 try-finally 更短更安全(避免漏关,避免 close 自己抛异常吞掉原异常)。
// TryWithResources.java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class TryWithResources {
public static void main(String[] args) throws IOException {
Path p = Files.createTempFile("demo", ".txt");
Files.writeString(p, "line1\nline2\nline3");
// 多个资源用分号分隔,关闭顺序与声明相反
try (BufferedReader r = Files.newBufferedReader(p)) {
String line;
while ((line = r.readLine()) != null) {
System.out.println(line);
}
}
// 走到这里时 r 已经被自动关闭
Files.delete(p);
}
}异常链:包装而非吞掉
当底层异常对上层不友好(比如把 SQLException 暴露给 Controller 是不合适的),通过包装把底层异常作为 cause 保留下来。再加 throw new XxxException("业务说明", e),调用栈和原始异常都能在日志里看到。
// ExceptionChain.java
public class ExceptionChain {
static String loadUser(long id) throws UserNotFoundException {
try {
// 假装是 SQL 查询
if (id <= 0) throw new RuntimeException("SQL error: bad id");
return "Alice";
} catch (RuntimeException dbErr) {
throw new UserNotFoundException("load user " + id + " failed", dbErr);
}
}
public static void main(String[] args) {
try {
loadUser(-1);
} catch (UserNotFoundException e) {
System.out.println(e.getMessage());
System.out.println("caused by: " + e.getCause());
// e.printStackTrace(); // 完整链
}
}
}
class UserNotFoundException extends Exception {
public UserNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}自定义异常
业务层往往需要自己的异常类型,便于调用方按类型 catch。约定俗成:业务异常多继承 RuntimeException(unchecked,避免到处 throws 污染签名),命名以 Exception 结尾。
// CustomException.java
public class CustomException {
public static void main(String[] args) {
try {
validate("");
} catch (ValidationException e) {
System.out.println(e.getField() + ": " + e.getMessage());
}
}
static void validate(String name) {
if (name == null || name.isBlank()) {
throw new ValidationException("name", "must not be blank");
}
}
}
class ValidationException extends RuntimeException {
private final String field;
public ValidationException(String field, String message) {
super(message);
this.field = field;
}
public String getField() { return field; }
}实践要点
- 永远不要写空 catch 块;至少 log 一下
- 不要 catch Exception 后什么都不做,再 throw 一个无 cause 的新异常——丢失栈信息
- 业务流程出错抛 RuntimeException;库强制 caller 处理才用 checked
- 底层异常对上层无意义时,包装:throw new XxxException("msg", originalException)
- try-with-resources 一律替代手写 finally close
- Spring 等框架几乎都把 checked 转成 unchecked——主流趋势是少用 checked