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. 为什么要自定义异常?

当现有异常类型无法满足需求时,需要自定义异常:

  • 提供更精确的业务错误信息
  • 让调用方针对性处理特定场景

自定义异常原则:

  1. 继承最接近的异常子类(无合适则继承 Exception
  2. 根据业务场景决定是否为检查型异常:
    • ✅ 可恢复情况 → 检查型异常
    • ❌ 不可恢复情况 → 非检查型异常

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. 重写带异常的方法时需遵守哪些规则?

规则总结:

  1. 父类无异常声明

    • ✅ 子类可抛出任意非检查型异常
    • ❌ 不可抛出检查型异常
    class Parent {
        void doSomething() { /* ... */ }
    }
    
    class Child extends Parent {
        void doSomething() throws IllegalArgumentException { /* ... */ } // 合法
    }
    
  2. 父类声明检查型异常

    • ✅ 子类可抛出:
      • 所有/部分/无父类声明的检查型异常
      • 范围更窄的异常子类
      • 任意非检查型异常
    • ❌ 不可抛出范围更宽或未声明的检查型异常
    class Parent {
        void doSomething() throws IOException, ParseException { /* ... */ }
        void doSomethingElse() throws IOException { /* ... */ }
    }
    
    class Child extends Parent {
        void doSomething() throws IOException { /* ... */ } // 减少异常
        void doSomethingElse() throws FileNotFoundException, EOFException { /* ... */ } // 更窄子类
    }
    
  3. 父类声明非检查型异常

    • ✅ 子类可抛出任意非检查型异常(无需关联)
    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 个核心面试题,覆盖基础概念、最佳实践和高级技巧。这些知识点是面试高频考点,建议结合实际项目经验深入理解。祝您面试顺利!


原始标题:Java Exceptions Interview Questions (+ Answers)