Java开发教程17-Optional

xianaldai 2026-08-18 03:17 1

本期是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 变空盒,否则装起来

ofofNullable 的区别:值确定不空用 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("玩家不存在"));

orElseorElseGet 的区别要害在备胎是"提前算好"还是"用到才算".关键看实参:



  • 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

  • 使用边界:用在返回值,别用在字段和参数

  • 配置读取改造小项目

    下一期我们进入多线程——程序第一次"同时干好几件事",也是整条进阶线里最陡的一个坡.




最新回复 (1)
  • fablia 08-18 03:34
    1

    我是来看nsfw的

    就最后那两图嘛,这不sfw

* 帖子来源Linux.do
返回