1. 概述
异常是每个 Java 开发者必须掌握的核心概念。本文整理了面试中常见的异常相关问题及答案,帮你快速复习关键知识点。
2. 面试题解析
Q1. 什么是异常?
异常是程序执行过程中发生的异常事件,它会中断程序的正常指令流。简单说,就是程序运行时出现的"意外情况"。
Q2. throw 和 throws 关键字的作用是什么?
throws
用于声明方法可能抛出的异常,强制调用方处理异常:
public void simpleMethod() throws Exception {
// ...
}
throw
用于手动抛出异常对象,通常在业务条件不满足时使用:
if (task.isTooComplicated()) {
throw new TooComplicatedException("任务太复杂了");
}
Q3. 如何处理异常?
使用 try-catch-finally
语句块:
try {
// 可能抛出异常的代码
} catch (ExceptionType1 ex) {
// 处理 ExceptionType1
} catch (ExceptionType2 ex) {
// 处理 ExceptionType2
} finally {
// 无论是否异常都会执行
}
✅ try
块包含可能出错的代码(称为"受保护代码")
✅ catch
块匹配并处理特定异常
✅ finally
块总会执行(即使发生异常或 return)
Q4. 如何捕获多个异常?
有三种方式:
方式一:使用通用异常处理器(不推荐)
try {
// ...
} catch (Exception ex) {
// 捕获所有异常
}
⚠️ 过于宽泛的异常处理会隐藏问题,导致代码难以维护
方式二:多个 catch 块
try {
// ...
} catch (FileNotFoundException ex) {
// 处理文件未找到
} catch (EOFException ex) {
// 处理文件结束
}
✅ 注意:子类异常必须先于父类异常捕获,否则编译报错
方式三:多捕获块(Java 7+)
try {
// ...
} catch (FileNotFoundException | EOFException ex) {
// 同时处理两种异常
}
✅ 减少代码重复,更易维护
Q5. 检查型异常和非检查型异常的区别?
特性 | 检查型异常 (Checked) | 非检查型异常 (Unchecked) |
---|---|---|
处理要求 | 必须处理或声明 | 可选处理 |
发生阶段 | 编译时检查 | 运行时出现 |
典型代表 | IOException, SQLException | RuntimeException, Error |
继承关系 | Exception 子类(非 RuntimeException) | RuntimeException 及其子类,Error 及其子类 |
Q6. 异常和错误的区别?
异常(Exception)表示可恢复的情况,错误(Error)表示通常无法恢复的外部问题。
常见 JVM 错误示例:
OutOfMemoryError
:内存耗尽,垃圾回收无法释放空间StackOverflowError
:线程栈空间耗尽(通常由无限递归导致)ExceptionInInitializerError
:静态初始化块抛出异常NoClassDefFoundError
:类加载器找不到类定义UnsupportedClassVersionError
:类文件版本不支持(如用高版本编译器生成)
⚠️ 虽然可以用 try-catch 捕获 Error,但不推荐!程序状态可能已损坏,无法可靠恢复。
Q7. 执行以下代码会抛出什么异常?
Integer[][] ints = { { 1, 2, 3 }, { null }, { 7, 8, 9 } };
System.out.println("value = " + ints[1][1].intValue());
抛出 ArrayIndexOutOfBoundsException
。因为 ints[1]
数组长度为 1(只有 null
元素),尝试访问索引 1 越界。
Q8. 什么是异常链?
当一个异常由另一个异常触发时形成异常链,帮助追踪问题根源:
try {
task.readConfigFile();
} catch (FileNotFoundException ex) {
throw new TaskException("任务执行失败", ex); // 保留原始异常
}
Q9. 什么是堆栈跟踪?它与异常有何关系?
堆栈跟踪(Stacktrace)显示从程序启动到异常发生时的完整调用链(类名和方法名)。它是调试利器,能精确定位异常抛出位置和根本原因。
Q10. 为什么要自定义异常?
当现有异常类型无法满足需求时,需要自定义异常:
- 提供更精确的业务错误信息
- 让调用方针对性处理特定场景
自定义异常原则:
- 继承最接近的异常子类(无合适则继承
Exception
) - 根据业务场景决定是否为检查型异常:
- ✅ 可恢复情况 → 检查型异常
- ❌ 不可恢复情况 → 非检查型异常
Q11. 异常机制的优势?
相比传统错误处理方式,异常机制优势明显:
- ✅ 关注点分离:核心业务逻辑与错误处理解耦
- ✅ 错误传播:沿调用栈自动向上传递,无需手动检查
- ✅ 分类处理:利用继承层次结构批量捕获同类异常
Q12. 能在 Lambda 表达式中抛出任意异常吗?
标准函数接口:只能抛出非检查型异常(因方法签名无 throws
声明):
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
if (i == 0) {
throw new IllegalArgumentException("零值不允许");
}
System.out.println(Math.PI / i);
});
自定义函数接口:可抛出检查型异常:
@FunctionalInterface
public static interface CheckedFunction<T> {
void apply(T t) throws Exception;
}
public void processTasks(
List<Task> tasks, CheckedFunction<Task> checkedFunction) {
for (Task task : tasks) {
try {
checkedFunction.apply(task);
} catch (Exception e) {
// 处理异常
}
}
}
// 使用示例
processTasks(taskList, t -> {
// ...
throw new Exception("任务处理异常");
});
Q13. 重写带异常的方法时需遵守哪些规则?
规则总结:
父类无异常声明:
- ✅ 子类可抛出任意非检查型异常
- ❌ 不可抛出检查型异常
class Parent { void doSomething() { /* ... */ } } class Child extends Parent { void doSomething() throws IllegalArgumentException { /* ... */ } // 合法 }
父类声明检查型异常:
- ✅ 子类可抛出:
- 所有/部分/无父类声明的检查型异常
- 范围更窄的异常子类
- 任意非检查型异常
- ❌ 不可抛出范围更宽或未声明的检查型异常
class Parent { void doSomething() throws IOException, ParseException { /* ... */ } void doSomethingElse() throws IOException { /* ... */ } } class Child extends Parent { void doSomething() throws IOException { /* ... */ } // 减少异常 void doSomethingElse() throws FileNotFoundException, EOFException { /* ... */ } // 更窄子类 }
- ✅ 子类可抛出:
父类声明非检查型异常:
- ✅ 子类可抛出任意非检查型异常(无需关联)
class Parent { void doSomething() throws IllegalArgumentException { /* ... */ } } class Child extends Parent { void doSomething() throws ArithmeticException, BufferOverflowException { /* ... */ } }
Q14. 以下代码能编译通过吗?
void doSomething() {
// ...
throw new RuntimeException(new Exception("链式异常"));
}
✅ 能通过。编译器只检查链式异常的第一个异常(RuntimeException
是非检查型异常),无需 throws
声明。
Q15. 如何在没有 throws
声明的方法中抛出检查型异常?
利用编译器类型擦除机制"欺骗"编译器:
public <T extends Throwable> T sneakyThrow(Throwable ex) throws T {
throw (T) ex; // 类型擦除后实际为 throw ex
}
public void methodWithoutThrows() {
this.<RuntimeException>sneakyThrow(new Exception("检查型异常")); // 伪装成非检查型
}
⚠️ 这是高级技巧,仅用于特殊场景(如框架设计),日常开发慎用!
3. 总结
本文梳理了 Java 异常机制的 15 个核心面试题,覆盖基础概念、最佳实践和高级技巧。这些知识点是面试高频考点,建议结合实际项目经验深入理解。祝您面试顺利!