Optional:把缺失写进类型
学完你要能自己回答
- Optional 想解决什么问题?它适合放在哪些位置?
map、flatMap、filter分别怎么用?为什么map可能得到嵌套 Optional?orElse和orElseGet的求值时机有什么差别?
意图:显式的「可能没有」01
Optional<T> 是一个最多装一个值的容器:Optional.empty() 或 Optional.of(value)。它的价值在方法签名上——调用方看到 Optional,就知道要处理缺失,而不是靠注释或约定。
- 适合:可能查不到的返回值,例如
findById、findFirst。 - 不适合:字段、方法参数、集合元素;这些位置用 null、重载或空集合更直接。
- 不适合:性能敏感的热点路径;每次
map/filter都会产生新对象。 - 不适合:序列化边界;Optional 不实现 Serializable,放进 DTO 或 RPC 模型会出问题。
Optional.of(null) 会立刻 NPE,get() 在空时抛 NoSuchElementException。它只是把「有没有值」变成类型信息。API 地图:创建、转换、取值02
| 方法 | 作用 | 空时行为 |
|---|---|---|
of / ofNullable / empty | 创建 | of(null) 抛 NPE |
isPresent / ifPresent | 判断 / 消费 | ifPresent 不执行 |
map / flatMap / filter | 转换 / 展平 / 过滤 | 返回 empty |
orElse / orElseGet / orElseThrow | 默认值或异常 | 按各自语义 |
get | 直接取值 | 抛 NoSuchElementException |
static Optional<String> find(String key) {
Map<String, String> db = new HashMap<>();
db.put("ada", "Ada Lovelace");
return Optional.ofNullable(db.get(key));
}
System.out.println(find("ada").map(String::toUpperCase).orElse("none"));
System.out.println(find("alan").map(String::length).orElse(-1));
System.out.println(find("ada").filter(s -> s.startsWith("A")).isPresent());
System.out.println(find("ada").flatMap(s -> Optional.of(s.split(" ")[0])).get());
ADA LOVELACE -1 true Ada
map 会自动把返回值包成 Optional;如果函数本身返回 Optional,用 flatMap 避免得到 Optional<Optional<T>>。filter 在条件不满足时直接变 empty。
get() 只应在已经确认有值时使用;更推荐 orElse、orElseGet、orElseThrow 或 ifPresent,让缺失处理留在链式调用里。orElse vs orElseGet:求值时机03
orElse(T other) 的参数在调用前就求值,不管 Optional 是否为空;orElseGet(Supplier) 只在为空时调用 Supplier。默认值只是一个常量时差别不大;默认值需要查库、创建对象或记录日志时,orElse 会带来无谓副作用。
static String expensive() {
System.out.println("expensive()");
return "fallback";
}
System.out.println(Optional.empty().orElse(expensive()));
System.out.println(Optional.of("value").orElse(expensive()));
System.out.println(Optional.of("value").orElseGet(() -> expensive()));
expensive() fallback expensive() value value
从输出可以看到:Optional 明明有值,orElse(expensive()) 仍然执行了 expensive();orElseGet 没有执行。默认值有副作用时,这个差别就是 bug。
orElseThrow 需要传 Supplier;无参 orElseThrow() 是 JDK 10 才加入的。写 8 的代码不要漏参数。反模式:不要把 Optional 当万能判空04
if (opt.isPresent()) opt.get():直接用map、ifPresent或orElse,不要手动拆箱。- 把 Optional 当字段或参数:序列化、RPC、ORM 都可能不认,调用方被迫拆箱。
- 返回
Optional<List<T>>:空列表已经能表达「没有元素」,用Collections.emptyList()。 - 在集合里放 Optional:用空集合或先过滤掉缺失元素。
- 用 Optional 包装基本类型:用
OptionalInt/OptionalLong/OptionalDouble,避免装箱。
try {
Optional.of(null);
} catch (NullPointerException e) {
System.out.println(e.getClass().getSimpleName());
}
NullPointerException
Optional.of 要求值非空,Optional.ofNullable 才接受 null。这个命名很容易记反;创建前先问自己「这个值允许为 null 吗」。
isEmpty()、or()、stream()、ifPresentOrElse();这些分别是 11、9、9、9 才补的。不要从新版本教程里直接抄。与 Stream 配合:展平缺失值05
Stream<Optional<T>> 很常见,例如批量查询后每个 id 得到一个 Optional。JDK 8 没有 Optional.stream(),需要先 filter(Optional::isPresent) 再 map(Optional::get)。
List<Optional<String>> list = Arrays.asList(
Optional.of("a"), Optional.empty(), Optional.of("b"));
List<String> values = list.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
System.out.println(values);
[a, b]
如果你发现自己频繁写 filter(isPresent).map(get),说明上游可能不该返回 Optional,或者应该提供一个批量查询接口,直接返回找到的结果集合。
Optional 只能保证容器本身不装 null;它包装的对象内部字段仍可能为 null。不要把 Optional 当成深层判空工具。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| `Optional` | 8 新增 | 随 Stream 一起进入标准库;不是独立的 JEP。 |
| `Optional.isEmpty` | 11 起 | JDK 8 用 !isPresent()。 |
| `Optional.or` | 9 起 | JDK 8 用 orElseGet 链式替代。 |
| `Optional.stream` | 9 起 | JDK 8 用 filter(isPresent).map(get)。 |
| `ifPresentOrElse` | 9 起 | JDK 8 需要 if/else 或分开处理。 |
| 无参 `orElseThrow()` | 10 起 | JDK 8 必须传 Supplier。 |
| `Optional` 序列化 | 不实现 Serializable | 不要放进 DTO、RPC 参数或持久化模型。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。