Skip to content

Optional ​

S1-027建议学进阶约 45 分钟JDK 21编程语言基础 · 函数式与 Stream

1. 本节定位 ​

Optional 用一个类型回答"这个方法可能返回空":Optional<User> 比 User 多了一层明确的语义——调用方会知道"这里可能没有值",而不是拿到 null 才发现。

本节讲清它的正确用法,以及一个更重要的问题:什么时候不该用它。这个类被误用的比例相当高。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 Optional 要解决的问题(null 的语义缺失);
  • 用 of / ofNullable / empty 正确创建;
  • 用 map / filter / flatMap 写链式转换,替代多层判空;
  • 说清 orElse 与 orElseGet 的关键区别;
  • 用 ifPresentOrElse 处理"存在与不存在"两种情况;
  • 说出三种不该使用 Optional 的场景,并知道替代写法。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

null 有三个问题:

第一,它不表达语义。 findUser("u404") 返回 null,是"用户不存在"?还是"查询失败"?调用方不知道。

第二,它容易漏判。 少写一次判空就是一次 NPE,而编译器不会提醒你。

第三,判空代码会层层嵌套。

java
// 传统写法:每多一层就多一个 if
User user = findUser(id);
if (user != null) {
    String email = user.getEmail();
    if (email != null) {
        return email.toUpperCase();
    }
}
return "无邮箱";
java
// Optional 写法:链路式表达
return findUser(id)
        .map(User::email)
        .map(String::toUpperCase)
        .orElse("无邮箱");

第二种写法不只是短,而是每一步的"可能为空"都被显式表达出来了。

5. 核心概念 ​

5.1 Optional 是一个容器 ​

java
Optional<User> user = findUser("u1");       // 要么装着 User,要么是空的容器

它最多装一个值。"可能没有值"这件事从方法签名上就看得见——这是它最大的价值。

5.2 三种创建方式 ​

java
Optional.of("值");              // 值必须非 null,否则抛 NullPointerException
Optional.empty();              // 明确的空
Optional.ofNullable(value);    // value 可能是 null(最常用)

选择标准:

场景用法
确定值非 null(空值属于编程错误)Optional.of(value) —— 快速失败
值可能为 null(如 map.get(key))Optional.ofNullable(value)
明确返回空Optional.empty()

5.3 取值:四个方法 ​

java
optional.get()                                     // 不推荐:为空时抛 NoSuchElementException
optional.orElse("默认值")                            // 为空时用默认值
optional.orElseGet(() -> "懒加载默认值")               // 为空时才计算默认值
optional.orElseThrow(() -> new IllegalStateException("必须存在"))

orElse 与 orElseGet 的关键区别:

java
// orElse:参数"总会被求值",无论 Optional 是否为空
String value = optional.orElse(loadFromDatabase());      // 数据库查询总是执行!

// orElseGet:只有为空时才执行 Supplier
String value = optional.orElseGet(() -> loadFromDatabase());   // 需要时才查

判断标准:默认值的计算有代价(查库、调接口、创建大对象)→ 必须用 orElseGet;只是一个字面量或常量 → orElse 更简洁。

get() 为什么被"不推荐":它把 Optional 变回了"可能抛异常的值",等于绕了一圈又回到手动判空,而且异常是 NoSuchElementException(比 NPE 更陌生)。

5.4 转换:map / filter / flatMap ​

java
findUser(id).map(User::email)                       // 转换:值存在时应用函数
findUser(id).map(User::email).filter(e -> e.contains("@"))   // 过滤:不满足则变空
findUser(id).flatMap(this::findEmailDetail)         // 展平:函数本身返回 Optional

map 与 flatMap 的区别:

java
// map:函数返回普通值 → 自动包装成 Optional
optional.map(user -> user.name())                   // Optional<String>

// flatMap:函数返回 Optional → 避免嵌套两层 Optional<Optional<String>>
optional.flatMap(user -> findEmail(user.id()))      // Optional<String>

如果对"返回 Optional 的函数"用 map,你会得到 Optional<Optional<String>>——这是 flatMap 存在的理由。

5.5 消费:存在时做什么 ​

java
optional.ifPresent(user -> System.out.println(user.name()));       // 存在时执行

optional.ifPresentOrElse(
        user -> System.out.println("找到: " + user.name()),
        () -> System.out.println("未找到"));                        // Java 9+:两种情况都处理

ifPresentOrElse 是 if-else 的表达式版本,避免了 isPresent() + get() 的写法。

5.6 与 Stream 配合 ​

java
List<String> ids = List.of("u1", "u404", "u2");
List<String> names = ids.stream()
        .map(this::findUser)              // Stream<Optional<User>>
        .flatMap(Optional::stream)        // Java 9+:把 Optional 展平成流,自动跳过空
        .map(User::name)
        .toList();

Optional::stream 是"批量查询时跳过空值"的标准写法——没有它就要写 filter(Optional::isPresent).map(Optional::get)。

5.7 三种不该用 Optional 的场景 ​

场景为什么不该用替代
字段增加序列化复杂度(JPA/Jackson 处理麻烦);字段为 null 本身可以接受直接用对象,或返回 Optional 的 getter
方法参数调用方要多写一层包装;参数可空可以用重载表达重载方法,或明确要求非 null
集合元素List<Optional<T>> 没有意义(不该往里放空值)直接不放这个元素
用于基本类型Optional<Integer> 有装箱开销OptionalInt / OptionalLong / OptionalDouble

一句话原则:Optional 是给"返回值"用的——它解决的问题是"调用方拿到 null 不知道该怎么办"。其他位置用它,通常是把简单问题复杂化。

6. 递进示例 ​

6.1 最小示例:创建与取值 ​

java
Optional<String> present = Optional.of("值");
Optional<String> empty = Optional.empty();

present.orElse("默认值");        // "值"
empty.orElse("默认值");          // "默认值"

6.2 业务示例:链式查询 ​

java
static Optional<User> findUser(String id) {
    return Optional.ofNullable(DATABASE.get(id));
}

// 查询邮箱,找不到给默认值
String email = findUser(id).map(User::email).orElse("无邮箱");

// 查询并校验
boolean valid = findUser(id)
        .map(User::email)
        .filter(e -> e.contains("@"))
        .isPresent();

6.3 反例:把 Optional 用成"更麻烦的 null" ​

java
// 反例一:isPresent + get(等于手动判空)
if (optional.isPresent()) {
    return optional.get().name();      // 与 if (obj != null) 相比没有改进
}

// 反例二:Optional 做字段/参数(见 5.7 节)

// 反例三:orElse 里放昂贵操作(每次调用都执行)
String name = optional.orElse(loadFromDatabase());      // 无论是否为空都会查库

7. 完整可运行代码 ​

文件:labs/functional/src/main/java/cn/kb/learn/functional/OptionalLesson.java

java
package cn.kb.learn.functional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

public class OptionalLesson {
    record User(String name, String email) {
    }

    private static final Map<String, User> DATABASE = Map.of(
            "u1", new User("张三", "zhangsan@test.com"));

    /** 查询可能找不到:返回 Optional 而不是 null */
    static Optional<User> findUser(String id) {
        return Optional.ofNullable(DATABASE.get(id));
    }

    public static void main(String[] args) {
        // 1. 三种创建方式
        Optional<String> present = Optional.of("值");
        Optional<String> empty = Optional.empty();
        Optional<String> nullable = Optional.ofNullable(null);
        System.out.println("of: " + present + ",empty: " + empty + ",ofNullable(null): " + nullable);

        try {
            Optional.of(null);
        } catch (NullPointerException exception) {
            System.out.println("Optional.of(null) 抛 NullPointerException");
        }

        // 2. 取值:orElse / orElseGet / orElseThrow
        System.out.println("orElse: " + empty.orElse("默认值"));
        System.out.println("orElseGet: " + empty.orElseGet(() -> "懒加载默认值"));
        try {
            empty.orElseThrow(() -> new IllegalStateException("值必须存在"));
        } catch (IllegalStateException exception) {
            System.out.println("orElseThrow: " + exception.getMessage());
        }

        // 3. 存在时执行
        findUser("u1").ifPresent(user -> System.out.println("ifPresent 找到: " + user.name()));
        findUser("u404").ifPresentOrElse(
                user -> System.out.println("ifPresentOrElse 找到: " + user.name()),
                () -> System.out.println("ifPresentOrElse 未找到,走 else 分支"));

        // 4. 链式转换:避免层层判空
        System.out.println("查询 u1 的邮箱: " + findUser("u1").map(User::email).orElse("无邮箱"));
        System.out.println("查询 u404 的邮箱: " + findUser("u404").map(User::email).orElse("无邮箱"));

        // 5. filter:条件不满足时返回空 Optional
        System.out.println("邮箱以 zhangsan 开头: "
                + findUser("u1").map(User::email).filter(email -> email.startsWith("zhangsan")).isPresent());
        System.out.println("邮箱以 other 开头: "
                + findUser("u1").map(User::email).filter(email -> email.startsWith("other")).orElse("被过滤掉了"));

        // 6. flatMap:避免 Optional 嵌套
        Function<User, Optional<String>> emailLookup = user -> Optional.ofNullable(user.email())
                .filter(email -> email.contains("@"));
        System.out.println("flatMap 链式查询: " + findUser("u1").flatMap(emailLookup).orElse("无"));
        System.out.println("对不存在的用户 flatMap: " + findUser("u404").flatMap(emailLookup).orElse("无"));

        // 7. 反例:isPresent + get(等于没解决空指针问题)
        if (findUser("u1").isPresent()) {
            System.out.println("不推荐的写法(isPresent + get): " + findUser("u1").get().name());
        }

        // 8. 与 Stream 配合:Optional::stream 自动跳过空值
        List<String> ids = List.of("u1", "u404", "u2");
        List<String> names = ids.stream()
                .map(OptionalLesson::findUser)
                .flatMap(Optional::stream)
                .map(User::name)
                .toList();
        System.out.println("批量查询(自动跳过不存在的): " + names);
    }
}

运行命令:

powershell
cd labs
./mvnw -pl functional compile
java -cp functional/target/classes cn.kb.learn.functional.OptionalLesson

预期输出:

text
of: Optional[值],empty: Optional.empty,ofNullable(null): Optional.empty
Optional.of(null) 抛 NullPointerException
orElse: 默认值
orElseGet: 懒加载默认值
orElseThrow: 值必须存在
ifPresent 找到: 张三
ifPresentOrElse 未找到,走 else 分支
查询 u1 的邮箱: zhangsan@test.com
查询 u404 的邮箱: 无邮箱
邮箱以 zhangsan 开头: true
邮箱以 other 开头: 被过滤掉了
flatMap 链式查询: zhangsan@test.com
对不存在的用户 flatMap: 无
不推荐的写法(isPresent + get): 张三
批量查询(自动跳过不存在的): [张三]

三处值得对照:

输出含义
ofNullable(null): Optional.empty把可能为 null 的值安全地包装起来
查询 u404 的邮箱: 无邮箱链式调用中任何一步为空,整体就走默认分支,不会 NPE
批量查询(自动跳过不存在的): [张三]flatMap(Optional::stream) 自动过滤掉了不存在的那两个 id

8. Debug 观察点 ​

  1. 在 findUser("u404").map(User::email) 处断点,观察 map 收到空 Optional 时直接返回空,不执行函数——这就是"不会 NPE"的原因;
  2. 在 ifPresentOrElse 的两个 lambda 里各打一个断点,分别用 u1 与 u404 触发,看走了哪个分支;
  3. 把 orElseGet(() -> "懒加载默认值") 改成 orElse(compute("默认值")),并在 compute 方法里打断点——你会发现即使 Optional 有值,它也会执行;
  4. 展开 Optional 对象,能看到内部的 value 字段(空时为 null)——它就是一个简单的容器。

9. 常见错误 ​

错误 1:get() 之前不判断 ​

java
Optional<User> user = findUser(id);
user.get().name();                    // 为空时抛 NoSuchElementException

修复:用 map + orElse / orElseThrow / ifPresent。get() 在规范的项目里应该被视为禁用方法。

错误 2:orElse 里放昂贵计算 ​

java
String value = optional.orElse(loadFromDatabase());        // 总是执行
String value = optional.orElseGet(() -> loadFromDatabase());   // 需要时才执行

错误 3:把 Optional 当字段或参数 ​

java
class User {
    private Optional<String> nickname;      // 反模式
}

void update(Optional<String> nickname) { }  // 反模式:调用方要写 Optional.ofNullable(...)

修复:字段直接用可空对象;参数用重载或明确非空。

错误 4:嵌套两层 Optional ​

java
Optional<Optional<String>> nested = optional.map(this::findEmail);    // 嵌套
Optional<String> flat = optional.flatMap(this::findEmail);            // 正确

错误 5:为了用而用 ​

java
// 不必要:值已经确定非空
String name = Optional.of(user.getName()).orElse("默认");    // 直接 user.getName() 就行

// 不必要:判空逻辑本来很简单
if (list != null && !list.isEmpty()) { }                     // 比 Optional 包装更清楚

判断标准:用 Optional 应该是为了让代码更清楚。如果它让代码更绕,就是误用。

10. 动手实验 ​

目标:把一段多层判空的代码改写成 Optional 链。

步骤:

  1. 运行第 7 节代码,核对输出;
  2. 写一段传统风格的多层判空代码:从"订单 → 用户 → 地址 → 城市"逐层取值,任意一层为 null 返回"未知";
  3. 用 Optional 链重写它(map + orElse),对比代码量;
  4. 给第 3 步加一个 filter:只在城市不为空且长度大于 1 时返回;
  5. 写方法 Optional<Order> findLatestOrder(List<Order> orders):返回金额最大的订单,列表为空时返回 Optional.empty();
  6. 用 orElseThrow 与 orElse 各调用一次第 5 步的方法,说明两种选择的适用场景。

验收方式:能说出第 6 步两种选择的区别("调用方能否在缺失时给出合理默认")。

扩展任务:对比 Optional<List<String>> 与 List<String>(空列表)两种表达"没有数据"的方式,说明各自适合什么场景。

11. 练习题 ​

基础题

  1. 判断:Optional.get() 在为空时会返回 null。(对 / 错)
  2. 填空:可能为 null 的值应该用 ______ 包装;只在为空时才计算默认值要用 ______。
  3. 简答:为什么不应该把 Optional 用作字段类型?

代码阅读题

java
record Config(String value) {}

Map<String, String> map = new HashMap<>();
map.put("timeout", "30");

Optional<String> timeout = Optional.ofNullable(map.get("timeout"));
System.out.println(timeout.map(String::toUpperCase).orElse("默认"));

Optional<String> missing = Optional.ofNullable(map.get("missing"));
System.out.println(missing.map(String::toUpperCase).orElse("默认"));
  1. 两行输出分别是什么?第二行为什么不会抛 NPE?

编程题

  1. 写一个"配置读取"工具:
    • Optional<String> get(String key):从内部 Map 读配置;
    • int getInt(String key, int defaultValue):读到就转成 int,读不到或格式错误就用默认值(不能用 try-catch 处理"读不到",但可以用它处理"格式错误");
    • String getRequired(String key):读不到就抛 IllegalStateException;
    • 写 main 演示三种情况。

综合任务

  1. 用 Optional 重构一段"用户信息聚合"逻辑:
    • 有 UserRepository(Optional<User> findById(String))、AddressRepository(Optional<Address> findByUserId(String));
    • 写方法 String describeCity(String userId):返回用户所在城市,用户或地址缺失时分别返回不同的提示(如"用户不存在"/"地址未填写");
    • 要求:不使用 if (x != null),全部用 Optional 链;
    • 说明:当"缺失原因"需要区分时,Optional 链是否仍然合适?如果不够,你会怎么补充信息?
分级提示

提示 1(方向):第 4 题的关键是 map 在空 Optional 上不会执行函数。

提示 2(关键点):第 5 题的 getInt 用 orElse(defaultValue) 处理"读不到",用 try-catch 包住 Integer.parseInt 处理"格式错误"——两者是不同的问题。

提示 3(接近答案):第 6 题区分"用户不存在"与"地址未填写"需要分别在两步用 orElse:

java
User user = userRepository.findById(userId).orElse(null);
if (user == null) return "用户不存在";
return addressRepository.findByUserId(userId).map(Address::city).orElse("地址未填写");

——注意这里 orElse(null) 又回到了 null,说明"需要区分多个缺失原因"时 Optional 链不够用,更好的做法是返回更丰富的结果类型(如 Result 或抛不同异常)。

12. 参考答案 ​

基础题
  1. 错。get() 在为空时抛 NoSuchElementException,不是返回 null。这也说明它为什么该被避免:它把"空"变成了异常,而不是让调用方优雅处理。
  2. Optional.ofNullable(value);orElseGet(() -> ...)。
  3. 因为:① 序列化框架(JPA、Jackson)处理 Optional 字段需要额外配置;② 字段为 null 本身是可接受的(对象刚创建时字段可能未赋值);③ 它会让"读写字段"多一层包装,收益却是零。Optional 的设计意图是"返回值"。
代码阅读题(第 4 题)
text
30
默认
  • timeout 有值,map(String::toUpperCase) 把 "30" 转换后返回,orElse 不触发;
  • missing 是空 Optional,map 直接返回空而不执行函数(所以不会对 null 调用 toUpperCase),orElse 返回 "默认"。

关键机制:map 内部会先判断是否有值,为空时直接返回 Optional.empty()。这就是 Optional 链"天然防 NPE"的原因。

编程题(第 5 题)
java
package cn.kb.learn.functional;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class ConfigReader {
    private final Map<String, String> values = new HashMap<>();

    public ConfigReader put(String key, String value) {
        values.put(key, value);
        return this;
    }

    public Optional<String> get(String key) {
        return Optional.ofNullable(values.get(key));
    }

    public int getInt(String key, int defaultValue) {
        return get(key)
                .map(text -> {
                    try {
                        return Integer.parseInt(text.trim());
                    } catch (NumberFormatException exception) {
                        return defaultValue;
                    }
                })
                .orElse(defaultValue);
    }

    public String getRequired(String key) {
        return get(key).orElseThrow(() -> new IllegalStateException("缺少必需配置: " + key));
    }

    public static void main(String[] args) {
        ConfigReader config = new ConfigReader()
                .put("timeout", "30")
                .put("retries", "abc");

        System.out.println("timeout: " + config.getInt("timeout", 10));
        System.out.println("retries(格式错误,用默认值): " + config.getInt("retries", 3));
        System.out.println("missing(不存在,用默认值): " + config.getInt("missing", 5));
        System.out.println("timeout 字符串: " + config.get("timeout").orElse("未配置"));

        try {
            config.getRequired("database.url");
        } catch (IllegalStateException exception) {
            System.out.println("预期内的失败: " + exception.getMessage());
        }
    }
}

注意 getInt 的分工:Optional 负责"键是否存在",try-catch 负责"值格式是否正确"——两件不同的事,用两种不同的机制,不要把格式错误也塞给 Optional。

预期输出:

text
timeout: 30
retries(格式错误,用默认值): 3
missing(不存在,用默认值): 5
timeout 字符串: 30
预期内的失败: 缺少必需配置: database.url
综合任务(第 6 题)
java
package cn.kb.learn.functional;

import java.util.Optional;

public class UserCityService {
    record User(String id, String name) {
    }

    record Address(String userId, String city) {
    }

    interface UserRepository {
        Optional<User> findById(String id);
    }

    interface AddressRepository {
        Optional<Address> findByUserId(String userId);
    }

    private final UserRepository userRepository;
    private final AddressRepository addressRepository;

    UserCityService(UserRepository userRepository, AddressRepository addressRepository) {
        this.userRepository = userRepository;
        this.addressRepository = addressRepository;
    }

    /** 纯 Optional 链写法(无法区分"用户不存在"与"地址未填") */
    String describeCitySimple(String userId) {
        return userRepository.findById(userId)
                .flatMap(user -> addressRepository.findByUserId(user.id()))
                .map(Address::city)
                .orElse("未知");
    }

    /** 需要区分缺失原因时:牺牲一点"纯净",换取准确的提示 */
    String describeCity(String userId) {
        Optional<User> user = userRepository.findById(userId);
        if (user.isEmpty()) {
            return "用户不存在";
        }
        return addressRepository.findByUserId(user.get().id())
                .map(Address::city)
                .filter(city -> !city.isBlank())
                .orElse("地址未填写");
    }

    public static void main(String[] args) {
        UserRepository users = id -> "u1".equals(id)
                ? Optional.of(new User("u1", "张三"))
                : Optional.empty();
        AddressRepository addresses = userId -> "u1".equals(userId)
                ? Optional.empty()
                : Optional.empty();

        UserCityService service = new UserCityService(users, addresses);
        System.out.println("纯链式(丢失原因): " + service.describeCitySimple("u404"));
        System.out.println("区分原因 - 用户不存在: " + service.describeCity("u404"));
        System.out.println("区分原因 - 地址未填: " + service.describeCity("u1"));
    }
}

关于"Optional 够不够用"的结论:

Optional 只表达"有 / 没有",不表达"为什么没有"。所以:

需求合适方案
缺失时用默认值Optional + orElse / orElseGet
缺失时需要区分原因抛不同的异常(《异常体系与处理》)、或返回自定义 Result 类型
缺失是可接受的结果就是 Optional 的最佳场景

这正是"工具要匹配问题"的体现:Optional 不是万能的,硬用它表达"多个失败原因",代码会比直接判空更绕。

预期输出:

text
纯链式(丢失原因): 未知
区分原因 - 用户不存在: 用户不存在
区分原因 - 地址未填: 地址未填写

13. 掌握标准 ​

  • [ ] 能用 ofNullable 包装可能为空的值;
  • [ ] 会用 map / filter / flatMap 写链式转换;
  • [ ] 能说清 orElse 与 orElseGet 的区别并按场景选择;
  • [ ] 知道 Optional::stream 在批量查询中的用法;
  • [ ] 能说出三种不该用 Optional 的场景;
  • [ ] 判断标准清晰:用它是为了更清楚,不是为了更"高级"。

14. 扩展资源 ​

15. 下一步学习 ​

S1 只剩最后三节:《线程与同步基础》、《Java 8 核心特性总览》、《Java 9-21 新特性与 Record》。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引