Java 中异常分哪两类,有什么区别?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 体系理解:面试官不仅仅是想知道 "受检异常" 和 "非受检异常" 这两个名词,更是想考察你是否理解 Java 异常体系的继承结构ThrowableError/Exception)。

  2. 编译机制:受检异常的 "受检" 是什么意思?这涉及 Java 编译器如何强制处理异常。

  3. 实践能力:实际开发中如何选择异常类型?何时用受检异常、何时用非受检异常?

核心答案

Java 异常分为受检异常和非受检异常两大类。受检异常必须显式处理(try-catchthrows),否则编译不通过;非受检异常可以不处理,由 JVM 在运行时抛出。

对比维度 受检异常 非受检异常
父类 Exception(除 RuntimeException ErrorRuntimeException
编译检查 ✅ 必须处理,否则编译失败 ❌ 不强制处理
处理方式 try-catchthrows 可选处理
代表场景 IO 错误、数据库异常 空指针、数组越界、内存溢出
设计理念 可恢复的异常 不可恢复或程序 bug

一句话总结:受检异常是 "编译器逼你处理",非受检异常是 "运行时随缘处理"。

深度解析

一、Java 异常体系结构

上图展示了 Java 异常体系的完整继承结构。关键要点:

  • Throwable:所有异常的父类,只有它的子类才能被 throwcatch
  • Error:JVM 级别的严重错误,程序无法恢复(如 OutOfMemoryErrorStackOverflowError
  • Exception:程序可以处理的异常
    • 受检异常IOExceptionSQLException 等,编译器强制处理
    • 非受检异常RuntimeException 及其子类,编译器不强制处理

二、受检异常 vs 非受检异常

import java.io.*;

public class ExceptionDemo {

    // ========== 受检异常:必须处理 ==========
    public void readCheckedFile(String path) {
        // ❌ 编译错误:Unhandled exception type IOException
        // FileReader reader = new FileReader(path);

        // ✅ 方式一:try-catch 捕获
        try {
            FileReader reader = new FileReader(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // ✅ 方式二:throws 声明抛出
    public void readFile(String path) throws IOException {
        FileReader reader = new FileReader(path);
    }

    // ========== 非受检异常:不强制处理 ==========
    public void uncheckedDemo() {
        String str = null;

        // ⚠️ 编译通过,但运行时抛出 NullPointerException
        System.out.println(str.length());

        // 可以选择捕获,也可以不捕获
        try {
            System.out.println(str.length());
        } catch (NullPointerException e) {
            System.out.println("空指针异常");
        }
    }
}

三、常见异常类型

类型 异常类 触发场景
Error(非受检) OutOfMemoryError 内存不足
StackOverflowError 栈溢出(无限递归)
NoClassDefFoundError 类找不到
RuntimeException(非受检) NullPointerException 访问 null 对象
ArrayIndexOutOfBoundsException 数组越界
ClassCastException 类型转换失败
ArithmeticException 算术异常(如除零)
IllegalArgumentException 非法参数
受检异常 IOException IO 操作失败
SQLException 数据库操作失败
ClassNotFoundException 类加载失败
InterruptedException 线程中断

四、如何选择异常类型?

上图展示了选择异常类型的决策流程。关键原则:

  • 受检异常:调用者可以合理恢复的情况(如文件不存在、网络超时)
  • 非受检异常:程序 bug 或调用者无法处理的情况(如空指针、数组越界)
  • Error:JVM 级别的严重问题,程序不应尝试捕获

五、最佳实践

// ✅ 好的做法:自定义业务异常(继承 RuntimeException)
public class BusinessException extends RuntimeException {
    private String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
}

// ✅ 好的做法:受检异常转非受检异常(避免污染方法签名)
public void processFile(String path) {
    try {
        readFile(path);
    } catch (IOException e) {
        // 将受检异常包装为非受检异常
        throw new RuntimeException("文件处理失败", e);
    }
}

// ❌ 不好的做法:捕获异常后不处理(吞掉异常)
try {
    doSomething();
} catch (Exception e) {
    // 什么都不做,异常被吞掉了!
}

// ✅ 好的做法:至少记录日志
try {
    doSomething();
} catch (Exception e) {
    log.error("操作失败", e);
    throw new BusinessException("OPERATION_FAILED", "操作失败");
}

面试高频追问

  1. throwthrows 有什么区别?

    关键字 位置 作用
    throw 方法体内 抛出一个异常对象
    throws 方法签名上 声明方法可能抛出的异常类型
  2. finalfinallyfinalize 有什么区别?

    • final:修饰符,类不能继承、方法不能重写、变量不能修改
    • finallytry-catch 的最后一块,无论是否异常都会执行
    • finalizeObject 的方法,GC 回收对象前调用(已废弃)
  3. 为什么要有受检异常?有人说它是 Java 的设计失误,你怎么看?

    争议点:

    • 支持者:强制处理异常,提高代码健壮性
    • 反对者:导致代码冗余,异常传播污染方法签名,实际中常被滥用
    • 趋势:现代框架(Spring)倾向于使用非受检异常

常见面试变体

  • 变体一:"ErrorException 有什么区别?"
  • 变体二:"RuntimeException 和普通 Exception 有什么区别?"
  • 变体三:"如何设计一个自定义异常类?"

记忆口诀

受检异常编译查,必须 try-catch 或 throws; 非受检异常运行时,不强制处理随缘抓; Error 严重难恢复,Exception 可处理。

总结

Java 异常分为受检异常(必须显式处理)和非受检异常(不强制处理)。受检异常适合可恢复的场景,非受检异常适合程序 bug 或无法恢复的场景。实际开发中,推荐使用非受检异常(继承 RuntimeException)设计业务异常,避免受检异常污染方法签名。