- xianaldai2026-08-1810677
本期是Java开发教程系列的第十七期,本期介绍 Optional 一个专门用来装"可能什么都没有"的值的容器.它让程序里成片的
if (xxx == null)消失,也是 Java 8 时代送给新手的"防空指针保险".若你已经熟练使用 Optional 的常用方法,可以跳过本期内容.天外音:想要的图在主贴最后了.
版权声明:图片部分来自于网络,如有侵权,可联系删除.
*每期叠甲*:
时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.
一、痛点回顾:空指针与防不胜防的 null
第 8 期我们讲过异常,其中最常见的就是
NullPointerException(空指针)——调用了一个 null 对象的方法,程序当场炸掉.空值从哪来?回想我们写过的代码:第 9 期
Integer.parseInt("abc")抛异常、文件读不到返回 null、map.get(键不存在)返回 null、list里可能藏着 null.只要有"查不到"“不存在”"没填"的场景,结果就可能是个 null.于是代码里长满了防御:```
String name = null;
if (map.containsKey("player")) {
name = map.get("player");
}
if (name != null) {
name = name.toUpperCase();
}
if (name != null && !name.isEmpty()) {
System.out.println("你好, " + name);
}
```写多了就变成"一半业务一半防空".而防御漏了一处,就是一次线上事故. Optional 的目标:把"可能为空"这件事变得显式且可控——类型上就告诉所有人"这里可能没有值",逼着你处理.二、Optional 是什么:一个装值的盒子
Optional<T>是一个盒子,里面要么有一个 T 类型的值,要么没有(空盒).创建方式三种:```
Optional<String> a = Optional.of("sword"); // 有值;传 null 会立刻抛异常(造盒时就知道错了)
Optional<String> b = Optional.empty(); // 空盒
Optional<String> c = Optional.ofNullable(x); // x 可能为 null:null 变空盒,否则装起来
```of和ofNullable的区别:值确定不空用of,可能为空用ofNullable.怎么把值取出来:```
Optional<String> box = Optional.ofNullable(maybeNull);if (box.isPresent()) { // 有没有值?像 if (x != null)
System.out.println(box.get()); // 取出来(盒子空时 get 抛异常,先用 isPresent 挡住)
}
``这样写只是把!= null` 换了个名字,没意义.Optional 真正的价值在下面三个方法.三、核心三件套:orElse / orElseGet / orElseThrow
取值的三种策略,正好对应三种业务场景:```
// 1. 有值用值,没值用备胎(备胎是已经算好的一个值)
String name = box.orElse("无名");// 2. 备胎是"需要时才计算"的——造备胎很贵时用它
// 注意:这里实参写的是 () -> loadFromDatabase(),一个 Supplier
String name2 = box.orElseGet(() -> loadFromDatabase());// 3. 没值就抛异常,调用方必须处理
String name3 = box.orElseThrow(() -> new IllegalStateException("玩家不存在"));
```orElse和orElseGet的区别要害在备胎是"提前算好"还是"用到才算".关键看实参:-
orElse(值):括号里是个值——调用orElse之前,这个值就已经算好了.所以哪怕box有值,实参里的表达式也会先算一遍.-
orElseGet(() -> ...):括号里是个工厂(Supplier),只有box为空时才调它去算.来看对比就明白了:把备胎从一个常量换成一次昂贵查询——```
// 有值也会先调用 loadFromDatabase()——白跑一次
String a = box.orElse(loadFromDatabase());
// 只有 box 为空时才调用——按需
String b = box.orElseGet(() -> loadFromDatabase());
``所以"无名"这种常量无所谓(常量本就常量);loadFromDatabase()这种很贵或不该每次都执行的调用,就必须用orElseGet——用orElse会无论box` 有没有值都先把备胎算出来,白白执行一次.orElseThrow则把"没有值"变成一次明确的失败:读配置读到空、查用户没查到,直接抛,后面代码根本不用防 null.配合方法链,一段典型的 Optional 代码长这样:```
String name = Optional.ofNullable(findPlayer())
.map(p -> p.name) // 玩家对象 → 名字(第四节)
.orElse("无名");
``没有一处if null`.四、map:盒子里的值继续加工
Optional.map和 Stream 的map思路一样:盒子有值,就应用函数转换;盒子为空,原样空着,绝不报错:```
Optional<String> box = Optional.ofNullable(maybeNull);
Optional<String> upper = box.map(s -> s.toUpperCase());
// 有值:盒子里是 "HELLO";没值:还是空盒
``最妙的是可以**连成一条链**,每一环都自动处理"空"的情况,不用每步都判:``
// 老写法,一层层判空
String city = null;
User user = findUser();
if (user != null) {
Address addr = user.getAddress();
if (addr != null) {
city = addr.getCity();
}
}// Optional 写法,每一步都跳过空
String city2 = Optional.ofNullable(findUser())
.map(u -> u.getAddress())
.map(a -> a.getCity())
.orElse("未知城市");
``对比一下:老写法两个 if 嵌套,新写法是**一条平铺的链**.每多一层属性,老写法多一层缩进,新写法只多一行map`.这就是 Optional 最舒服的用法——链式取属性,全链防空.五、什么时候用,什么时候别用
Optional 是"返回值的约定",不是万能的:
推荐用: 方法的返回值 "查不到东西"用
Optional表达,调用方想忽略空就orElse,想报错就orElseThrow,选择权交给他不推荐用: 字段 每个对象都带个 Optional 盒子,占用大、序列化麻烦,不如"该 null 就 null"
- 方法参数——参数必传或可选,
null或@Nullable标注足够表达,Optional 参数会让调用方很难受Optional 出现在返回值上.和 Stream 一样,它是工具,不是金科玉律——项目里大量
if (x != null)且逻辑简单的场景,老老实实写 if 完全没问题.六、小项目:配置读取改造
模拟游戏里读取配置:配置中心可能查不到某个值,传统写法层层判空,改成 Optional 链.
文件
ConfigService.java(假装这是"配置中心"的接口):```
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;public class ConfigService {
private Map<String, String> config = new HashMap<>();public ConfigService() {
config.put("volume", "80"); // 音量默认 80
config.put("language", "zh_cn");
// 故意不放 "difficulty" 这一项
}// 查配置,查不到返回 Optional.empty(),不返回 null
public Optional<String> get(String key) {
return Optional.ofNullable(config.get(key));
}
}
``文件Main.java:``
import java.util.Optional;public class Main {
public static void main(String[] args) {
ConfigService config = new ConfigService();// 音量,有值用值,没有用默认 80(顺带转成 int)
int volume = config.get("volume")
.map(s -> Integer.parseInt(s))
.orElse(80);
System.out.println("音量: " + volume);// 难度,不存在 → 用默认值,不打日志不报错
String difficulty = config.get("difficulty").orElse("normal");
System.out.println("难度: " + difficulty);// 语言,查不到必须让启动失败(这配置不能没有)
String language = config.get("language")
.orElseThrow(() -> new IllegalStateException("缺少 language 配置"));
System.out.println("语言: " + language);
}
}
``运行结果:``
音量: 80
难度: normal
语言: zh_cn
``动手试试:把config.put("volume", "80")注释掉,看音量走orElse(80);再把config.put("language", ...)` 注释掉,看程序抛异常——同一个"查不到",三种处理方式,全靠调用方自己选.朝花夕拾:第 16 期那个
OptionalDouble第 16 期我们写过一行:
list.stream().mapToDouble(s -> s.score).max(),当时说返回值"是 Optional 包装,第 17 期讲"那行的返回类型其实是
OptionalDouble,和Optional<String>是亲兄弟:空列表时max()返回的就是"空盒",用orElse(0.0)就能安全取值:```
double max = list.stream().mapToDouble(s -> s.score).max().orElse(0.0);
```看,流和 Optional 天然是"一对"流处理的是"一堆可能没有结果的数据",Optional 接住"结果可能不存在"的那一下.这两个一起用,程序里几乎没有裸奔的 null 了.总结
本期学会了与"空"和平共处:
- Optional 是盒子:
of有值、empty空盒、ofNullable自适应- 核心三件套:
orElse给备胎、orElseGet按需给备胎、orElseThrow明确失败- map 链:属性一路取,全链防空,替代层层 if
- 使用边界:用在返回值,别用在字段和参数
- 配置读取改造小项目
下一期我们进入多线程——程序第一次"同时干好几件事",也是整条进阶线里最陡的一个坡.

主楼 - fablia2026-08-185831