乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

throws 与 throw

本节定位:明确 throws 和 throw 两个关键字在语法、语义和使用场景上的本质区别。掌握用 throws 声明方法契约、用 throw 手动抛出异常对象,以及异常链机制如何实现根因追溯。


场景引入

"白歌,我这里有个问题。"飞翔科技的小崔指着自己的代码,"我在方法签名字写了个 throws IOException,但同事说我还应该把 SQLException 也声明上去,到底什么情况下该用 throws?"

架构师白歌走过来看了一眼:"throws 和 throw 是两回事。throws 是告诉调用方'我这个方法可能会出问题',throw 是你在方法里面主动说'现在出问题了'。一个是声明,一个是动作。"

旁边的李眉也凑过来:"那什么时候用哪个?我经常搞混。"

大翔笑道:"记住一句话——throws 在方法签名上(声明契约),throw 在方法体里面(抛出动作)。一个是承诺书,一个是行动。"


定义对比

维度throwsthrow
关键字类型方法声明的一部分可执行语句
位置方法签名中,参数列表后、方法体前方法体内部
作用声明该方法可能抛出哪些异常真正抛出一个异常对象
跟随内容异常类名(可以多个,逗号分隔)异常对象(new 出来的实例)
对调用方影响调用方必须 try-catch 或继续 throws无直接影响(但异常会向上传播)
编译行为编译器检查调用方是否处理了受检异常编译器不强制检查

throws:方法契约的异常声明

语法与使用

修饰符 返回类型 方法名(参数列表) throws 异常类型1, 异常类型2, ... {
    // 方法体
}
/**
 * 从数据库中查询学生信息
 *
 * @param studentId 学生ID
 * @return 学生对象
 * @throws SQLException 数据库操作异常(受检异常,调用方必须处理)
 * @throws IllegalArgumentException 学生ID为空(非受检异常,声明是为了文档化)
 */
public Student findStudent(String studentId)
        throws SQLException, IllegalArgumentException {

    if (studentId == null || studentId.isEmpty()) {
        throw new IllegalArgumentException("学生ID不能为空");  // 非受检,可以不声明
    }

    // JDBC 操作可能抛出 SQLException(受检异常,必须声明或捕获)
    Connection conn = DataSource.getConnection();
    PreparedStatement ps = conn.prepareStatement(
        "SELECT * FROM students WHERE id = ?");
    ps.setString(1, studentId);
    ResultSet rs = ps.executeQuery();
    // ...
}

throws 的继承规则

当子类重写父类方法时,throws 声明遵循严格的规则:

规则说明示例
不能抛出更宽泛的异常子类不能声明父类方法未声明的新的受检异常父类方法无 throws,子类不能加 throws IOException
可以抛出更具体的异常子类可以声明父类异常的子类父类 throws Exception,子类可以 throws IOException
可以不声明异常子类可以不抛任何异常父类 throws IOException,子类可以不 throws
可以增加非受检异常子类可以增加任意 RuntimeException不受限制
// 父类接口
interface StudentRepository {
    Student findById(String id) throws DataAccessException;
}

// ✅ 正确:不声明异常
class InMemoryStudentRepo implements StudentRepository {
    @Override
    public Student findById(String id) {  // 可以不 throws
        return storage.get(id);
    }
}

// ✅ 正确:声明更具体的异常
class DatabaseStudentRepo implements StudentRepository {
    @Override
    public Student findById(String id) throws SQLException {  // SQLException 是 DataAccessException 的子类
        // JDBC 操作
    }
}

// ❌ 编译错误:声明了更宽泛或无关的受检异常
class BrokenStudentRepo implements StudentRepository {
    @Override
    public Student findById(String id) throws Exception {  // 编译错误!Exception 比 DataAccessException 宽泛
        // ...
    }
}

throw:手动抛出异常

语法

throw 异常对象实例;

throw 是一个语句,必须跟一个 Throwable 的实例(通常是 new 出来的)。

使用场景

场景一:参数校验

public void enrollStudent(Student student) {
    // 防御性编程:参数校验
    if (student == null) {
        throw new IllegalArgumentException("学生对象不能为 null");
    }
    if (student.getName() == null || student.getName().trim().isEmpty()) {
        throw new IllegalArgumentException("学生姓名不能为空");
    }
    if (student.getAge() < 6 || student.getAge() > 100) {
        throw new IllegalArgumentException("学生年龄不合法: " + student.getAge());
    }

    // 业务逻辑
    studentDao.save(student);
}

场景二:业务规则违反

public void transferCredits(String fromStudentId, String toStudentId, int amount) {
    Student from = studentDao.findById(fromStudentId);
    Student to = studentDao.findById(toStudentId);

    if (from == null) {
        throw new StudentNotFoundException("转出学生不存在: " + fromStudentId);
    }
    if (to == null) {
        throw new StudentNotFoundException("转入学生不存在: " + toStudentId);
    }
    if (from.getCredits() < amount) {
        throw new InsufficientCreditsException(
            "学分不足,需要 " + amount + ",当前只有 " + from.getCredits());
    }

    from.setCredits(from.getCredits() - amount);
    to.setCredits(to.getCredits() + amount);
    studentDao.update(from);
    studentDao.update(to);
}

场景三:异常转换(包装)

将底层异常包装为上层有意义的异常:

public List<Student> importFromFile(String filePath) throws StudentImportException {
    try {
        List<String> lines = Files.readAllLines(Paths.get(filePath));
        return parseStudents(lines);
    } catch (IOException e) {
        // 将底层 IOException 包装为业务异常
        throw new StudentImportException("导入学生数据失败: " + filePath, e);
    }
}

异常链:追溯根因

什么是异常链

异常链是指将一个异常(原始原因 cause)关联到另一个异常(上层包装),形成一条从表层到根因的追溯链。

两种设置 cause 的方式

// 方式一:构造方法传入 cause(推荐)
throw new StudentImportException("导入失败", ioException);

// 方式二:通过 initCause() 方法(已废弃的用法,仅了解)
StudentImportException ex = new StudentImportException("导入失败");
ex.initCause(ioException);  // 只能调用一次
throw ex;

自定义异常支持异常链

// 自定义异常需要提供接收 cause 的构造方法
public class StudentImportException extends Exception {

    public StudentImportException(String message, Throwable cause) {
        super(message, cause);  // 调用 Throwable(String, Throwable) 构造器
    }
}

完整示例:异常链的追踪

public class ExceptionChainDemo {

    // 底层方法:抛出原始异常
    public static String readFileContent(String path) throws IOException {
        throw new FileNotFoundException("文件不存在: " + path);  // 根因
    }

    // 中间层:包装原始异常
    public static String parseConfig(String path) throws ConfigException {
        try {
            return readFileContent(path);
        } catch (IOException e) {
            throw new ConfigException("配置加载失败", e);  // 包装,保留根因
        }
    }

    // 上层:再次包装
    public static void initSystem() throws SystemInitException {
        try {
            String config = parseConfig("/etc/app/config.properties");
            System.out.println("配置内容: " + config);
        } catch (ConfigException e) {
            throw new SystemInitException("系统初始化失败", e);
        }
    }

    public static void main(String[] args) {
        try {
            initSystem();
        } catch (SystemInitException e) {
            // 打印完整异常链
            System.err.println("=== 异常链分析 ===");
            System.err.println("表层异常: " + e);

            Throwable cause = e.getCause();
            int level = 1;
            while (cause != null) {
                System.err.println("原因层级 " + level + ": " + cause);
                cause = cause.getCause();
                level++;
            }

            // 打印完整堆栈(包含所有层级的 suppressed 和 cause)
            System.err.println("\n=== 完整堆栈 ===");
            e.printStackTrace();
        }
    }
}

// 自定义异常类(支持异常链)
class ConfigException extends Exception {
    public ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}

class SystemInitException extends Exception {
    public SystemInitException(String message, Throwable cause) {
        super(message, cause);
    }
}
运行输出:
=== 异常链分析 ===
表层异常: SystemInitException: 系统初始化失败
原因层级 1: ConfigException: 配置加载失败
原因层级 2: java.io.FileNotFoundException: 文件不存在: /etc/app/config.properties

=== 完整堆栈 ===
SystemInitException: 系统初始化失败
    at ExceptionChainDemo.initSystem(ExceptionChainDemo.java:21)
    ...
Caused by: ConfigException: 配置加载失败
    at ExceptionChainDemo.parseConfig(ExceptionChainDemo.java:13)
    ...
Caused by: java.io.FileNotFoundException: 文件不存在: /etc/app/config.properties
    at ExceptionChainDemo.readFileContent(ExceptionChainDemo.java:6)
    ...

异常传播机制

当一个异常在方法中抛出但未被捕获时,异常会沿着调用栈向上传播。

传播规则:

  1. 方法抛出异常 → JVM 查找当前方法的异常表
  2. 如果异常表中有匹配的 catch → 跳转到 catch 块
  3. 如果没有匹配的 catch → 方法立即结束,异常抛给调用方
  4. 调用方重复步骤 1-3
  5. 如果 main() 也没有捕获 → 线程终止,JVM 打印异常栈
public class PropagationDemo {

    public static void level3() {
        System.out.println("进入 level3");
        throw new RuntimeException("level3 抛出的异常");
    }

    public static void level2() {
        System.out.println("进入 level2");
        level3();  // 不捕获,向上传播
        System.out.println("离开 level2");  // 这行不会执行
    }

    public static void level1() {
        System.out.println("进入 level1");
        try {
            level2();
        } catch (RuntimeException e) {
            System.out.println("level1 捕获异常: " + e.getMessage());
            // 可以在这里重新抛出或处理
        }
        System.out.println("离开 level1");  // 这行会执行(因为异常被捕获了)
    }

    public static void main(String[] args) {
        System.out.println("=== 异常传播演示 ===");
        level1();
        System.out.println("程序正常结束");
    }
}
运行输出:
=== 异常传播演示 ===
进入 level1
进入 level2
进入 level3
level1 捕获异常: level3 抛出的异常
离开 level1
程序正常结束

飞翔科技实战:学生管理系统中的异常契约

场景

架构师白歌定义了学生管理服务的异常契约:

/**
 * 学生管理服务接口
 * 方法签名中的 throws 就是双方约定的"异常契约"
 */
public interface StudentService {

    /**
     * 注册新学生
     * @throws DuplicateStudentException 学号已存在
     * @throws InvalidStudentException  学生信息不合法
     */
    void registerStudent(Student student)
        throws DuplicateStudentException, InvalidStudentException;

    /**
     * 导入学生数据
     * @throws StudentImportException 导入过程中的任何异常
     */
    int importStudents(InputStream data)
        throws StudentImportException;
}

小崔的实现:

@Service
public class StudentServiceImpl implements StudentService {

    @Override
    public void registerStudent(Student student)
            throws DuplicateStudentException, InvalidStudentException {

        // 1. 参数校验:抛出非受检异常或受检异常
        if (student == null) {
            throw new IllegalArgumentException("学生对象不能为空");  // 非受检
        }
        if (student.getStudentId() == null) {
            throw new InvalidStudentException("学号不能为空");       // 受检,已声明
        }

        // 2. 业务规则校验
        Student existing = studentDao.findByStudentId(student.getStudentId());
        if (existing != null) {
            throw new DuplicateStudentException(
                "学号 " + student.getStudentId() + " 已被 " + existing.getName() + " 使用");
        }

        // 3. 数据库操作
        try {
            studentDao.save(student);
            logger.info("学生注册成功: {} - {}", student.getStudentId(), student.getName());
        } catch (DataAccessException e) {  // Spring 的数据访问异常(非受检)
            // 包装为受检的业务异常,保留根因
            throw new StudentServiceException("保存学生信息失败", e);
        }
    }

    @Override
    public int importStudents(InputStream data) throws StudentImportException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(data))) {
            int count = 0;
            String line;
            while ((line = reader.readLine()) != null) {
                Student student = parseStudent(line);
                registerStudent(student);  // 可能抛出 DuplicateStudentException
                count++;
            }
            return count;
        } catch (IOException e) {
            throw new StudentImportException("读取导入数据失败", e);
        } catch (DuplicateStudentException e) {
            throw new StudentImportException("导入数据包含重复学号", e);
        }
    }

    private Student parseStudent(String line) throws InvalidStudentException {
        String[] parts = line.split(",");
        if (parts.length < 3) {
            throw new InvalidStudentException("数据格式错误,需要至少3个字段: " + line);
        }
        return new Student(parts[0], parts[1], Integer.parseInt(parts[2]));
    }
}

易错场景

反例一:throws 了不该声明的非受检异常

// ❌ 错误:在 throws 中声明 RuntimeException 是不必要的
// 这样做会误导调用方以为需要处理
public void doSomething() throws NullPointerException {
    // NullPointerException 是 RuntimeException,不应该声明
}

// ❌ 更糟糕:声明了但实际不处理
public void caller() {
    try {
        doSomething();
    } catch (NullPointerException e) {
        // 用 try-catch 处理 NPE 而不是修复代码逻辑
        System.out.println("NPE 发生了");
    }
}
// ✅ 正确:非受检异常不应该在 throws 中声明
// (除非是为了文档化目的,但不应强制调用方处理)
public void doSomething() {
    // 通过代码逻辑防御 NPE
    // 如果真的想文档化,用 @throws javadoc 标签而非方法签名中的 throws
}

反例二:throw 了不应该抛的异常

// ❌ 错误:用异常做正常流程控制
public Student findStudent(String id) {
    Student student = studentDao.findById(id);
    if (student == null) {
        throw new StudentNotFoundException("学生不存在: " + id);
        // 异常不应该用于可预见的正常流程
    }
    return student;
}
// ✅ 正确方案一:返回 Optional(JDK 8)
public Optional<Student> findStudent(String id) {
    return Optional.ofNullable(studentDao.findById(id));
}

// ✅ 正确方案二:返回 null 并文档化
/**
 * @return 学生对象,如果不存在返回 null
 */
public Student findStudent(String id) {
    return studentDao.findById(id);
}

经验法则:异常应该用于"异常情况"——不应该用于可预见的、正常的业务流程控制。查找不到学生是正常情况,不应该用异常。

反例三:重新抛出时丢失原始异常

// ❌ 错误:包装异常时没有传入 cause
try {
    studentDao.save(student);
} catch (SQLException e) {
    // 丢失了原始异常的堆栈信息
    throw new StudentServiceException("保存失败");
}
// ✅ 正确:保留原始异常作为 cause
try {
    studentDao.save(student);
} catch (SQLException e) {
    throw new StudentServiceException("保存失败", e);  // 传入 cause
}

面试考点

Q1:throws 和 throw 有什么区别?

throws 是方法声明的一部分,写在方法签名中,声明该方法可能抛出的异常类型(可声明多个),用于告知调用方需要处理哪些异常。throw 是方法体内的可执行语句,用于实际抛出一个异常对象实例。记忆口诀:throws 是"声明",throw 是"动作"。

Q2:子类重写父类方法时,throws 声明有什么限制?

子类重写方法不能声明比父类方法更宽泛的受检异常(即不能声明父类未声明的新的受检异常)。但子类可以:①不声明任何异常;②声明父类异常的子类;③声明任意非受检异常(RuntimeException)。这个规则保证了里氏替换原则——用子类替代父类时,调用方不会遇到意料之外的受检异常。

Q3:什么是异常链?为什么需要它?

异常链是指将一个异常(根因)关联到另一个异常(包装),通过 Throwable 的 cause 属性形成链接。需要它的原因是:底层异常(如 SQLException)对上层调用方没有业务意义,需要包装为业务异常(如 StudentServiceException),但必须保留根因以便调试和日志分析。通过构造函数 new Exception(message, cause) 或 initCause() 方法设置。

Q4:什么时候用 throw,什么时候用 throws?

在方法体内部,需要主动报告错误时用 throw(如参数校验失败、业务规则违反)。在方法签名中,声明该方法可能抛出的受检异常时用 throws。原则:如果异常是方法无法处理的,就 throws 声明出去让调用方处理;如果能在方法内部处理,就 try-catch 而不用 throws。

Q5:throws 声明了 RuntimeException 会怎样?

编译不会报错,调用方也不会被强制处理。但在方法签名中声明 RuntimeException 会误导调用方以为这是一个需要处理的异常。Java 社区的最佳实践是:非受检异常不放在 throws 中(偶尔为了 javadoc 文档化用 @throws 标签说明,但不在方法签名中强制声明)。

上一页
try-with-resources
下一页
自定义异常