Java 异常体系(完整面试题 + 代码示例)
这个问题虽然看着简单,但是很多中型公司都会问,有什么很让你说出一些异常的类型,以及一些处理方式
一、什么是 Java 异常体系
简单来说,Java 异常体系是 Java 为了处理程序运行中出现的「非正常情况」(比如文件找不到、数组越界、空指针)而设计的一套标准化体系,核心是把所有异常情况归类,让开发者能清晰识别、捕获和处理这些问题。

(1)Error(错误)—— 程序 “救不回来” 的严重问题
-
特点:由 JVM 抛出,属于系统级错误,程序无法捕获、也处理不了,一旦出现,JVM 大概率会终止线程 / 程序。
-
核心逻辑:这是 “硬件 / 虚拟机层面的故障”,比如内存不够、栈溢出,开发者写代码时不用处理,也处理不了。
(2)Exception(异常)—— 程序 “能抢救” 的问题
特点: 程序运行时的业务 / 逻辑错误,开发者可以通过代码捕获并处理,让程序继续运行。
细分两类:
-
Checked 异常(受检异常):编译时就要求必须处理(要么catch捕获,要么throws声明抛出),不处理代码编译不过。比如读取文件时的FileNotFoundException。
-
Unchecked 异常(非受检异常):编译时不要求处理,运行时才会抛出,继承自RuntimeException。比如空指针、数组越界,是开发者代码写得不够严谨导致的。
| 类型 | 继承关系 | 编译时是否要求处理 | 能否被程序处理 | 典型例子 |
|---|---|---|---|---|
| Error | 直接继承 Throwable | 否 | 否 | StackOverflowError、OOM |
| Checked 异常 | 继承 Exception(非 Runtime) | 是 | 是 | IOException、SQLException |
| Unchecked 异常 | 继承 RuntimeException | 否 | 是 | NullPointerException、数组越界 |
import java.io.FileInputStream;
import java.io.IOException;
public class ExceptionStructureDemo {
public static void main(String[] args) {
// 1. Unchecked异常(运行时抛出)
String str = null;
// str.length(); // 运行时抛NullPointerException
// 2. Checked异常(编译期必须处理)
try {
FileInputStream fis = new FileInputStream("test.txt");
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. throw 和 throws 的区别是什么?各自的使用场景?
throw:
1.用在方法内部,手动抛出一个异常对象
2.语法:throw new ExceptionType("异常信息");
3.场景:主动触发业务异常(如参数不合法)
throws:
1.用在方法签名,声明该方法可能抛出的异常类型
2.语法:public void method() throws ExceptionType1, ExceptionType2 {}
3.场景:将异常向上抛出,由调用方处理
public class ThrowThrowsDemo {
public static void main(String[] args) {
try {
checkAge(15); // 调用方必须处理声明的异常
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
// 方法声明可能抛出的异常(Unchecked异常可省略throws)
public static void checkAge(int age) throws IllegalArgumentException {
if (age < 18) {
// 手动抛出异常对象
throw new IllegalArgumentException("年龄必须≥18岁,当前:" + age);
}
System.out.println("年龄验证通过");
}
}
3. finally 代码块一定会执行吗?哪些场景下 finally 不会执行?
通常情况: finally 一定会执行,无论 try 正常结束还是 catch 捕获异常,用于释放资源(如关闭流)
不会执行的场景:
1.调用 System.exit(0) 终止 JVM
2.执行 finally 前线程被中断 / 杀死
3.死循环 / 无限递归导致程序无法进入 finally
public class FinallyDemo {
public static void main(String[] args) {
System.out.println(testFinally()); // 输出20(finally修改num不影响返回值)
// System.exit(0); // 若在此处调用,finally不会执行
}
public static int testFinally() {
int num = 10;
try {
num /= 0; // 触发算术异常
return num;
} catch (ArithmeticException e) {
num = 20;
return num; // 先暂存返回值,再执行finally
} finally {
num = 30;
System.out.println("finally执行,num=" + num);
// return num; // 若finally有return,会覆盖try/catch的返回值(不推荐)
}
}
}
4. 重写方法时,异常声明有什么限制?
子类重写父类方法时,不能抛出比父类更宽泛的异常:
1.若父类方法未声明异常,子类重写时也不能声明 Checked 异常(可抛 Unchecked 异常)
2.若父类方法声明了异常,子类重写时只能抛出相同或其子类的 Checked 异常
3.Unchecked 异常(RuntimeException/Error)不受此限制,可随意抛出
class Parent {
public void readFile() throws Exception {
// 父类声明抛出Exception
}
}
class Child extends Parent {
// ✅ 合法:抛出父类异常的子类
@Override
public void readFile() throws IOException {
super.readFile();
}
// ❌ 非法:抛出更宽泛的异常(Throwable比Exception宽泛)
// public void readFile() throws Throwable {}
// ✅ 合法:Unchecked异常不受限制
public void readFile() throws NullPointerException {}
}
5. 自定义异常的实现方式?为什么要自定义异常?
实现方式:
1.继承 Exception(自定义 Checked 异常)或 RuntimeException(自定义 Unchecked 异常)
2.提供无参构造、带消息的构造、带消息 + 原因的构造
为什么要自定义:
1.区分业务异常与系统异常,便于定位问题
2.携带业务上下文信息(如用户 ID、错误码)
3.统一异常处理逻辑(如全局异常拦截)
// 自定义Checked异常
class UserNotFoundException extends Exception {
public UserNotFoundException() {
super();
}
public UserNotFoundException(String message) {
super(message);
}
public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用自定义异常
public class CustomExceptionDemo {
public static void main(String[] args) {
try {
findUserById(999);
} catch (UserNotFoundException e) {
System.out.println("业务异常:" + e.getMessage());
e.printStackTrace();
}
}
public static void findUserById(int id) throws UserNotFoundException {
if (id < 1 || id > 100) {
throw new UserNotFoundException("用户ID不存在:" + id);
}
System.out.println("找到用户:" + id);
}
}

浙公网安备 33010602011771号