查询不到商品,还是转换程序出错

商品查询返回 Guava Optional<Product>,空结果表示目录没有这个商品。迁移到 JDK Optional<Product> 时,常见改动是替换 import,再把 transform 改成 map。若转换函数不小心返回 null,旧实现抛异常,新实现却得到 empty;调用者随后返回“商品不存在”,原本的转换错误被折叠进正常缺失。

两种 Optional 都可以表达一个非 null 值或缺失,但相似的数据模型并不意味着所有操作相同。默认值何时计算、空返回值如何处理、异常类别、公开方法的返回类型以及 Java 序列化都可能改变。迁移需要逐项固定业务约束,而不是把编译通过当作全部验收。

本章固定 Guava 33.5.0-jre,使用 Java 8 API,在 Zulu 8u472、Corretto 21.0.11 上运行。第 05 篇区分检查与消息求值,这里沿着相同的参数求值规则检查 Optional 默认值。

容器不存 null,容器引用仍可能是 null

Guava Optional.fromNullable(null) 返回 absent,JDK Optional.ofNullable(null) 返回 empty;两种 of(null) 都拒绝 null。这表达的是容器内的值约束。Java 变量本身仍然可以被赋为 null,因此“方法返回 Optional”不能凭类型自动保证返回的容器引用非 null。接口文档、构造约束与测试仍需维持这条规则。

空容器上调用 get 也有差异:Guava 抛 IllegalStateException,JDK 抛 NoSuchElementException。如果原来有捕获特定异常的逻辑,直接迁移就会改变控制流。正常的查询接口最好明确处理缺失,而非把 get 的异常当成预期分支。Guava Optional、JDK Optional

商品不存在与商品解析失败也应分开表达。Optional 只保留有值和无值两种状态;如果导入需要报告行号、字段名及失败原因,把所有解析异常捕获后返回 empty 会丢掉诊断信息。只有业务已经决定不需要这些区别时,才能主动做这种合并。

transform 与 map 的 null 分支

Guava 固定源码把有值与缺失实现放在 Present、Absent 中。Present.transform 执行函数后用 checkNotNull 检查返回值,再建立新的 Present;Absent.transform 不调用函数,但检查函数引用非 null。这解释了为什么“输入 absent 不计算”和“有值函数返回 null 抛异常”可以同时成立。Present 源码、Absent 源码

JDK map 则在转换结果为 null 时返回 empty。若旧代码依赖 Guava 的失败语义,迁移后的 mapper 应显式使用 Objects.requireNonNull;如果业务原本就认为转换结果可能缺失,则可以接受 empty,但应把这项语义调整列入变更说明。相同输入集合必须同时包含“已有值但转换返回 null”的反例,仅测 absent 和普通字符串不足以发现差异。

下面的完整示例保留旧的“有值转换不应返回 null”约束。它使用标准 Optional,但没有把 mapper 缺陷转换成合法缺失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

public final class StrictOptionalMapping {
static <T, R> Optional<R> mapStrict(Optional<T> input, Function<T, R> mapper) {
Objects.requireNonNull(input, "optional");
Objects.requireNonNull(mapper, "mapper");
return input.map(value -> Objects.requireNonNull(mapper.apply(value), "mapped value"));
}
public static void main(String[] args) {
if (!mapStrict(Optional.of("SKU-1"), String::length).equals(Optional.of(5))) {
throw new AssertionError();
}
try {
mapStrict(Optional.of("SKU-1"), value -> null);
throw new AssertionError("expected null rejection");
} catch (NullPointerException expected) {
System.out.println(expected.getMessage());
}
}
}

这个适配只处理 map 的值与 null 约束,并不声称复刻 Guava 的全部异常消息、序列化形式或类型身份。对于简单迁移,局部约束通常比重新实现一个 Optional 容器更清楚。若源代码原本没有调用 transform,也没有必要为了迁移预先引入通用适配层。

默认值有三项独立差异

Guava 的 or(defaultValue) 和 JDK 的 orElse(defaultValue) 都在进入方法前求值 defaultValue。即使容器有值,构造默认商品、访问缓存或增加计数也已经发生。实验对两个有值容器分别传递带计数的字符串表达式,最终计数为 2,两个容器仍返回原来的 present 字符串。

切换到 Guava or(Supplier) 或 JDK orElseGet(Supplier) 后,有值分支不执行 supplier 方法体。本章计数保持为 2,确认方法体没有额外执行;这不等于证明 lambda 本身没有分配,也不构成吞吐结论。若 supplier 在别处已被提前调用并把结果传进来,惰性语义当然无法补救那个更早的调用。JLS 参数求值规则

第二项差异是 null 默认值。Guava Optional.of("present").or((String) null) 即使不会使用默认值,也抛 NullPointerException;JDK 的 orElse(null) 在有值时正常返回 present。Guava 的 Present 实现在返回引用前检查了默认值,而不是只在缺失时检查。

第三项差异是 supplier 的返回结果。Guava absent 的 supplier 若返回 null,同样被拒绝;JDK empty 的 orElseGet 可以返回 null。这样一来,调用方在 Optional 链结束后仍可能得到 null。选择哪种行为应取决于公开方法是否允许返回 null,而不能把“前面使用过 Optional”作为不再校验的理由。

转换函数保留的是哪一种空

Guava 在 JRE 版本提供 fromJavaUtil、toJavaUtil 转换。对非 null 容器,present 和 absent 都能对应转换;静态转换方法还保留输入的 null 容器引用,返回 null。它们没有把 null 容器修复成 empty。固定源码的三元表达式直接体现这个分支。Optional 转换源码

如果应用接口要求返回容器永远非 null,应在适配边界检查源容器,或明确决定把违规 null 折叠为空。后一种选择会隐藏上游违反协议的情况,必须有业务理由。Optional.toJavaUtil(googleOptional) 与非 null 实例上的 googleOptional.toJavaUtil() 在这一点上不能无条件互换。

公开类型也没有因此变成兼容。com.google.common.base.Optional 与 java.util.Optional 是不同的类;把 SDK 方法返回类型改掉,会影响调用者的编译和链接。更稳妥的迁移通常是在内部先使用目标类型,在旧公开入口做明确转换,等调用方迁移后再删除旧入口。本章通过真实转换断言和类身份检查确认这项边界;它没有冒充已运行跨版本 SDK 的二进制兼容实验。

旧式函数接口的适配方向

在本章固定 Guava 版本中,com.google.common.base.Function 扩展了 JDK java.util.function.Function,旧接口实例可以赋给标准接口变量。Guava Predicate 同样扩展 JDK Predicate,并用默认 test 委托旧的 apply。因此不少调用可以平滑向 JDK 函数接口移动,但反方向不成立:一个普通的 JDK Function 实例未必实现 Guava 子接口。

需要把标准函数传给仍接收 Guava Function 的接口时,modernFunction::apply 可以建立一个符合目标类型的适配器。Predicate 对应使用方法引用或 lambda 适配。实验验证这两个方向中的可执行路径;源码阅读核验继承与委托关系,而不是根据两个接口都只有一个主要方法就推断它们是同一个类型。Function 源码、Predicate 源码

函数对象的相等性也不应当作跨实现迁移依据。旧接口源码明确提醒不要依赖某些函数实现曾经提供的 equals 行为。把函数作为缓存键、把两个 lambda 的 equals 结果当作“相同行为”的证明,都会把实现细节扩大成协议。这里仅把函数当作执行转换的对象。

缺失传播不能替代所有失败处理

Optional 链把缺失向后传播,因此有值与无值的路径会执行不同数量的函数。若转换函数顺带更新审计状态或统计计数,切换数据分布也会改变副作用次数。本文用计数器观察这一点,只是实验观测手段;业务映射函数通常更适合保持单一的值转换职责,审计等必需动作应放在明确执行的位置。

迁移时还应检查异常是否继续向外传播。map 处理的是转换结果,不能据此推断它会把 mapper 抛出的异常变成 empty。若把一个查询函数包装为 Optional 链,却希望数据库错误也变成“无商品”,必须另外编写捕获逻辑;这一做法是否正确取决于调用协议,通常不能由容器选择替代判断。

空字符串也不是缺失容器。Optional.of("") 包含一个存在的空字符串,与 empty 分支不同。商品编号是否允许空串应由字段校验处理;只有字段协议明确把空串当缺失时,才能在转换前后加入相应规则。null、空串、空白和 absent 的区别继续适用第 01 篇的输入矩阵。

另一个常见设计是集合查询。返回空列表可以表达查询成功但没有元素,外面再包 Optional 通常又增加一层状态。新增状态应有具体含义,例如查询尚未执行;如果没有这个需求,调用方就要处理多个表示相同结果的组合。对于按唯一商品身份查询,Optional 的零或一个结果模型更直接;对于商品批量列表,应先明确是否真的存在第二种缺失。

类型边界越靠近共享 SDK,迁移就越应关注调用方。内部私有函数替换容器,主要影响局部行为;公开字段、方法参数、返回值和序列化对象都可能把库类型传到模块外。迁移清单应从这些真实使用点生成,而不只搜索 import 行。编译错误会暴露一部分差异,反射、持久化数据和预编译调用者仍需独立的验收场景。

相同类型名也不适合作为自动替换的唯一匹配条件。两个 Optional 的完整包名不同,or 的重载含义也不同,源码中已有的 method reference 可能受重载解析影响。固定版本文档还特别提示 toJavaUtil 同时存在静态和实例形式时,方法引用可能产生歧义,显式 lambda 更容易表达所选路径。迁移后的编译检查应和行为矩阵一起保留。

Java 序列化覆盖整个对象图

Guava Optional 实现 Serializable,JDK Optional 没有实现。实验把 Guava Optional<String> 写入 ObjectOutputStream 后读回,值相等;absent 也能完成往返。JDK 的 Optional 在相同路径抛 NotSerializableException。这指的是 Java 原生对象序列化,不能推广为“JDK Optional 无法表达成 JSON”,JSON 框架是否支持是另一套映射协议。

容器可序列化也不代表任何内容都可序列化。把普通 new Object() 放入 Guava Optional 后,序列化仍失败,因为被遍历到的对象不满足要求。测试只反序列化本进程刚生成的字节,并在 try-with-resources 中关闭输入输出流,不读取外部不可信序列化数据。Java 序列化规范

如果字段类型正从 Guava Optional 迁移到标准 Optional,持久化对象、消息载荷和历史数据需要单独评估。两个容器携带相同的字符串,并不意味着旧字节流可以原样读取。本章没有制造历史类版本迁移样本,因此只证明当前固定类路径下的往返与失败行为,不声明旧持久化数据已兼容。

实测、手算与速查

完整测试在两个 JDK 各完成 4 个测试,失败、错误、跳过均为 0。运行说明包含运行命令。正常值、缺失值、null mapper 结果、eager/supplier 默认值、静态转换的 null 引用、函数适配和对象流都由断言验收。
迁移位置 Guava 33.5.0-jre JDK 8 Optional
有值转换得到 null transform 抛 NPE map 返回 empty
空值 get IllegalStateException NoSuchElementException
有值且 null 默认值 or 拒绝 orElse 返回已有值
缺失且 supplier 返回 null 拒绝 可以返回 null
Java 对象序列化 容器支持,内容仍有限制 容器不实现 Serializable

手算题:of("SKU-1") 经过返回 null 的转换,再取默认值。Guava 路径能得到默认值吗?不能,transform 已抛异常;JDK map 路径则进入 empty,随后可得到默认值。若这代表 mapper 缺陷,应在 JDK 路径保留显式非 null 检查。

改动练习:为旧查询入口增加标准 Optional 的内部实现,旧入口只负责适配。分别测试普通商品、无商品、意外 null 容器及 mapper 返回 null,要求每种结果都与书面的接口约束一致,再检查公开签名中是否仍暴露两种容器类型。

可迁移做法 适用边界
迁移时同时比较值、异常与求值次数 同名容器提供相似但不相同的操作
把内部值转换与公开类型迁移分开验收 SDK、持久化对象或调用方依赖旧类型

系列入口:可复现基线。前篇:Preconditions 与 Verify。