Optional
Optional<T> 是一个容器,用来表示存在一个非空的 T 值,或者没有结果。它主要用于方法返回值,让调用方从类型上看到缺失分支。本文从可选查询结果入手,讲解默认值与转换操作,再说明参数、字段和集合中的使用边界。
1. 面试核心
先记住这几条,再读正文- 缺失分支。 方法返回
Optional<T>时,返回类型明确表达了结果存在或缺失这两种情况。卡 · 可选结果 - 默认值求值。
orElse在调用前计算默认值,orElseGet只在结果缺失时调用生成默认值的函数。 - map 与 flatMap。 映射函数返回
null时,map得到空Optional,flatMap则抛出NullPointerException。卡 · 转换函数的空值边界 - 缺失原因。
Optional.empty()只表示无值,不携带缺失原因。 - 容器相等性。
Optional是基于值的类,比较两个容器的内容应使用equals。
2. 可选结果
查询用户时,查不到记录也可以是正常结果。下面用 Optional<User> 将这两种情况写进返回类型:
Optional<User> findUser(long userId) {
User user = repository.find(userId);
return Optional.ofNullable(user);
}
返回 User 时,调用方无法从类型判断 null 是否是合法结果。返回 Optional<User> 后,缺失成为方法契约的一部分。
根据是否允许传入空值,可以选择以下三种创建方式:
Optional<String> present = Optional.of("Java");
Optional<String> absent = Optional.empty();
Optional<String> uncertain = Optional.ofNullable(value);
of(value)要求value非空,否则抛出NullPointerException。ofNullable(value)在参数为null时返回空Optional。empty()明确创建没有值的结果。
返回类型既然已经是 Optional<T>,就始终返回一个非空的 Optional 对象。用 null 表示“没有 Optional”,会重新引入两层空值判断。
3. Optional 结果处理
拿到 Optional 后,调用方需要决定结果缺失时做什么。直接调用 get() 会在缺失时抛出 NoSuchElementException,而下面的 API 可以明确表达各个分支。
3.1 缺失默认值
缺失属于正常情况时,可以用 orElse 提供默认值。比如,查不到用户时显示匿名名称:
String displayName = findUser(userId)
.map(User::displayName)
.orElse("anonymous");
map 只在用户存在时执行转换。任一步没有结果,最终使用默认名称。
3.2 默认值的延迟计算
默认值需要查询数据库时,要区分立即求值和延迟求值。Supplier 表示一个无参数、返回结果的函数,orElseGet 会在需要默认值时才调用它。
// 即使 cachedUser 有值,仍会执行数据库查询
User eager = cachedUser.orElse(loadFromDatabase());
// 只有 cachedUser 为空时,才执行数据库查询
User lazy = cachedUser.orElseGet(this::loadFromDatabase);
orElse接收计算好的默认值,所以loadFromDatabase()在进入orElse前就已执行。orElseGet接收函数,所以缓存有值时可以省去这次查询。- 默认值需要创建对象或执行有副作用的操作时,也应考虑延迟计算,避免无用工作。
3.3 缺失异常
当前业务要求用户必须存在时,可以用 orElseThrow 抛出业务异常:
User user = findUser(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
查询层只表达是否找到用户,业务层再决定缺失是否属于错误。因此,同一个查询既能用于显示匿名名称,也能用于要求用户存在的操作。
3.4 有值回调
只需要在有值时执行一个动作,可以使用 ifPresent。下面的代码只为查到的用户记录审计信息:
findUser(userId).ifPresent(user -> audit(user.id()));
如果存在和缺失都要处理,可以用 ifPresentOrElse 指定两个分支。分支包含多步业务时,普通 if 更容易看清执行顺序,也便于调试。
4. map、flatMap 与 filter
可选结果还可以继续转换或筛选。选择哪种操作,取决于下一步是返回普通值、返回另一个 Optional,还是判断一个条件。
4.1 map 值转换
map 在有值时执行转换,并把转换结果放进新的 Optional。比如,从查询结果中取出邮箱:
Optional<String> email = findUser(userId)
.map(User::email);
用户不存在时,映射函数不会执行。映射结果为 null 时,map 会得到空 Optional。
4.2 flatMap 容器展开
如果转换方法本身已经返回 Optional,可以用 flatMap 直接接续这个结果。下面先查用户地址,再从地址中取出城市:
Optional<Address> findAddress(User user) {
return Optional.ofNullable(user.address());
}
Optional<String> city = findUser(userId)
.flatMap(this::findAddress)
.map(Address::city);
使用 map(this::findAddress) 会得到 Optional<Optional<Address>>,多出一层没有新增语义的容器。
4.3 filter 条件过滤
filter 保留满足条件的值,条件不满足时返回空 Optional。下面只保留活跃用户:
Optional<User> activeUser = findUser(userId)
.filter(User::active);
空结果可表示用户不存在,也可表示用户不活跃。需要区分原因时,使用明确的结果类型或异常。
5. 适用边界
Optional 最适合在方法返回处表达缺失。把它用于参数、实体字段或集合外层时,还要判断额外的容器是否表达了业务需要的差异。
5.1 可选返回值
以下四类可选结果适合使用 Optional:
- 按键查询可能不存在的数据。
- 从集合中查找第一个符合条件的元素。
- 解析一个允许缺失、但格式错误需要单独处理的字段。
- 在多个可选步骤之间建立简短转换链。
调用方看到返回类型,就知道需要处理查不到结果的情况。查询仍可正常返回空容器,无需把每次缺失都作为异常抛出。
5.2 方法参数
参数可省略时,优先用重载、参数对象或明确允许 null 的约定表达。把参数改成 Optional,会让每个调用方都先包装一次:
// 不推荐:调用方还要先创建 Optional
void send(Optional<String> subject) {}
如果包装只是把空值判断移到所有调用点,接口会更繁琐。框架或领域专用语言(DSL)也可以约定用 Optional 作为输入协议。采用这种约定时,应说明包装带来的用途,以及缺失的业务含义。
5.3 实体字段
实体字段需要先确认框架能否正确读取和保存 Optional。JPA 实体、传输对象(DTO)和配置对象,都依赖框架识别属性。容器字段会影响属性发现、序列化格式或代理机制,所以采用前要确认原生支持情况。
跨服务传输时,用 JSON Schema、OpenAPI 或序列化协议声明字段是否可选。这些协议描述的是外部数据格式,接收方才能据此判断字段缺失的含义。
5.4 集合空值语义
Optional<List<Order>> 会区分以下三种状态:
- 空
Optional表示没有列表。 - 包含空列表的
Optional表示存在列表,但列表没有元素。 - 包含非空列表的
Optional表示存在订单。
如果前两种状态在业务上没有差异,直接返回空列表更简单:
List<Order> findOrders(long userId) {
return repository.findAllByUserId(userId);
}
两种空状态具有不同含义时,可保留额外层次。命名或结果类型应明确表达该差异。
6. Optional 的局限
Optional 管理的是值的存在性。对象内部是否合法、失败原因是什么,仍由业务代码处理。
6.1 链路中的空指针
使用容器后,创建和转换过程仍会抛出异常。下面两处都需要留意:
Optional.of(user) // user 为 null 时仍会抛异常
.map(User::profile) // 方法内部仍可能抛异常
.map(Profile::name);
of(user) 要求用户非空,而 profile() 内部也可以访问到空字段。因此,对象内部状态仍需业务校验,映射函数抛出的异常会向外传播。
6.2 缺失原因
空 Optional 只表示没有值。数据不存在可以是正常缺失,而权限不足、远程服务超时或数据损坏需要各自处理。调用方需要区分原因时,应使用异常、带状态的结果类型或领域对象。
6.3 复杂链式代码
混合转换、副作用和异常处理的长链会增加阅读成本。Optional 更适合简短的可选转换,复杂逻辑可用普通分支。
6.4 对象身份
Optional 是基于值的类,也就是内容相等的实例应当可以互换使用。因此,比较内容应使用 equals,判断是否有值应使用公开 API。== 比较的是容器身份,而把容器用作同步锁也依赖身份,两种用法都应避免。
7. 转换函数的空值边界
转换函数返回 null 时,map 和 flatMap 的行为不同。map 按 ofNullable 的规则包装普通值,所以会得到空容器。flatMap 直接使用返回的容器,所以要求容器引用本身非 null。
Optional<String> name = Optional.of("java");
System.out.println(name.map(v -> (String) null)); // Optional.empty
// name.flatMap(v -> (Optional<String>) null); // NullPointerException
示例中,map 把空值转换为空结果,而执行注释中的 flatMap 会抛出 NullPointerException。如果转换函数自己抛出异常,两种操作都会将异常向外传播。
Optional.stream() 可以把可选结果接入 Stream:有值时返回单元素流,缺失时返回空流。因此,处理一组可选结果时,可以用 flatMap(Optional::stream) 保留有值的元素。
8. 常见问题
8.1 什么时候使用 isPresent() 后再 get()?
分支包含多步操作时,可以先检查存在性再取值。下面的代码只转换名称,也可以直接用 map 表达:
if (user.isPresent()) {
return user.get().displayName();
}
只需转换、默认值或缺失异常时,map、orElse 和 orElseThrow 能直接表达结果。这样把有值和缺失的处理放在一起,也更容易检查是否遗漏了分支。
8.2 orElse 和 orElseGet 有什么区别?
orElse 的参数在调用前求值。orElseGet 只在缺失时调用供应函数。常量默认值可用前者,查询、创建对象或有副作用的操作可用后者。
8.3 Optional.ofNullable(value).orElse(null) 有意义吗?
这段代码可以适配必须返回 null 的旧接口。它先包装可空值,又把空容器转回 null,所以调用方仍需原来的空值判断。内部逻辑应保持一种缺失表示,只在适配旧接口时转换。
8.4 Optional 会带来多少性能开销?
开销取决于调用频率和运行环境,需要通过分析与基准测试判断。容器和函数调用会增加工作,但即时编译器(JIT)可以消除部分短命对象的分配。
普通业务返回值优先保证含义清楚。极热循环、数值路径和大批量对象处理,再比较实际开销。数值场景可以对比 OptionalInt 等专门类型与基本类型,选择满足性能要求的表示。
9 · 面试题卡片
9.1 Optional 适合解决什么问题,使用时有哪些边界
出现公司:网易有道、CVTE、快手
考察重点
- 解释
Optional怎样让调用方从返回类型看到缺失分支。 - 说明怎样根据转换结果和缺失含义,选择
map、flatMap、orElseGet或orElseThrow。 - 说明容器能处理的空值,以及仍需异常或结果类型表达的失败原因。
相关内容:可选结果、Optional 结果处理、适用边界、Optional 的局限。
参考回答
Optional<T> 适合表示允许缺失的方法返回值,让调用方从类型上看到缺失分支。
存在时,用 map 转换普通值,或用 flatMap 接续另一个可选结果。缺失时,用 orElse 提供默认值,用 orElseGet 延迟计算,或用 orElseThrow 抛出业务异常。
容器只管理有没有值,映射函数内部仍会抛出空指针等异常。权限不足、超时等不同原因,也需要异常或结果类型来区分。
用于参数时要考虑调用方的包装负担,用于实体字段时要确认框架支持。空集合已足够表达含义时,直接返回集合。返回 Optional 的方法则应始终返回非 null 容器。
9.2 Optional 的 map 和 flatMap 返回 null 时有什么区别?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:区分转换后的值为 null,与转换函数本应返回的容器为 null。
相关内容:转换函数的空值边界。
参考回答
转换函数返回 null 时,map 返回空 Optional,flatMap 抛出 NullPointerException。
原因是 map 负责包装普通值,采用 ofNullable 的规则。flatMap 则直接使用转换函数提供的容器,要求这个容器引用非空。两者都会向外传播转换函数自己抛出的异常。