读完这篇,你能做什么
能把判断和转换作为参数传递,并显式处理可缺失结果。
学习理由、准备要求与知识点
为什么学它
理解行为参数与空结果,是正确使用 Stream 和异步 API 的必要基础。
先读这些内容
知识点清单
- 函数式接口单抽象方法
- Predicate
/Function等职责 - Lambda 目标类型
- 方法引用匹配
- effectively
final捕获 - Optional 存在与缺失
- map
/filter/orElseGet - 副作用与共享对象
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
想把“怎样判断”或“怎样转换”作为参数传给方法,可以使用 Lambda。它为一个函数式接口提供实现;这种接口只有一个需要实现的抽象方法,也可以有默认或静态方法,某些 Object 公有方法签名不额外计数。Predicate 用来判断,Function 用来转换,Consumer 接收并处理值,Supplier 提供值。先看接口的方法参数和返回值,再读 Lambda,会更容易理解。
方法引用把部分 Lambda 写得更短,例如 String::length 可以表示把字符串转换成长度,是否适用仍取决于目标接口类型。Lambda 可以使用外部局部变量,但变量必须是 final 或初始化后不再赋值。这个限制只管变量本身:它指向的数组或对象仍可能被修改,也不会因此自动变得线程安全。
Optional 常用于明确表达一个方法可能没有结果。of 要求非空,ofNullable 把 null 转成空,map 转换存在值,flatMap 用于转换函数本身返回 Optional 的场景。它不表示所有异常都应该变成空,也不要求所有字段和参数都套一层 Optional。先说明“没有结果”是不是正常业务状态,再决定接口如何表达。
文中这几个词,先认一认
- 目标类型
- Lambda 要交给哪一种函数式接口,哪一种就是它的目标类型;例如交给
Predicate<String>时,它应接收String并返回boolean。编译器按这个接口的要求核对参数和返回值。 - 事实上的 final
- 局部变量虽没写
final,但初始化后从未重新赋值,因此可以被 Lambda 使用。 - 惰性求值
- 先提供计算方式,等确实需要结果时才运行它,避免提前做无用计算。
- Optional 空值
- 表示这个方法按约定没有结果;它本身并不说明是查无数据、输入错误还是其他原因。
02看看它怎么工作
Lambda 创建与 Lambda 执行是两个时刻。把读取文件的行为放进 Supplier,并不会在声明 Supplier 那一行就必然读取;调用 get 才执行相应逻辑。但普通方法参数表达式仍会先求值,所以 optional.orElse(expensive()) 会先调用 expensive,即使 Optional 已有值。orElseGet 接收行为,只有缺失时才调用它。
Optional.map 的转换结果如果是 null,会得到空 Optional。flatMap 的转换方法则必须返回一个 Optional,不能返回 null,但可以返回 Optional.empty();这样就不会套成 Optional<Optional<T>>。
没有值时直接调用 get 会抛异常,通常可以用 map、filter、orElseGet 或 orElseThrow 把处理方式写清楚。如果默认值计算很慢,或计算时会产生其他影响,还要考虑是否只在缺少值时才计算它。
把行为写成函数,不代表它每次都只计算一个值:Predicate 可以读当前时间,Function 可以写数据库,Supplier 可以每次返回不同结果。所以要说清它会被调用几次、遇到异常怎么办,以及何时提前停止。图中的提供者链按顺序尝试,找到结果后就结束;排在后面的提供者连检查和调用都不会执行。这就是这里的惰性规则:暂时不需要的步骤,留到真正需要时才做。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["按优先级排列 Supplier"] --> B["调用当前提供者"]
B --> C{"Optional 是否存在值"}
C -->|"是"| D["返回并停止"]
C -->|"否"| E{"还有提供者"}
E -->|"是"| B
E -->|"否"| F["返回空 Optional"]
B --> G["异常按契约传播"]按顺序看,每一步做了什么
- 从函数式接口签名判断 Lambda 类型。
- 区分创建行为与执行行为。
- 明确捕获变量和对象可变性。
- 用 Optional 表达正常缺失。
- 测试默认计算次数、短路和异常传播。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/util/Optional.java · java.util.Optional#orElseGet(Supplier)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.util.function.Supplier;
public final class SourceModel {
public static <T> T choose(T present, Supplier<? extends T> fallback) {
if (present != null) return present; // 存在时不调用备用行为。
return fallback.get();
}
}真实 Optional.orElseGet 检查内部是否存在值,只有缺失分支才调用 supplier.get。模型用一个可空引用代表内部状态,把三元表达式展开成分支,不模拟 Optional 的对象表示和其它操作。它展示的重点是调用位置,默认结果不是在传参时就已经计算好。
如果调用方写 orElse(buildDefault()),buildDefault 作为普通实参表达式会先执行。即使已有值最终被返回,默认构建的耗时或副作用也已经发生。改成 orElseGet(() -> buildDefault()) 才把行为延后;这不是 Optional 特有魔法,而是参数从已计算值变成了可调用对象。
真实惰性方法在已有值时不需要调用 supplier,因此不能为了方便在模型入口无条件执行它。空 Optional 遇到 null supplier 会失败,已有值则不会走到调用点。接口设计若希望无条件拒绝 null 提供者,可以另行声明更严格前提,但必须承认与真实方法的分支行为不同。
Supplier 只说明“无参数调用并返回结果”,不保证非空、没有副作用或永远只运行一次。orElseGet 在一次缺失调用中执行它一次,再调用还可能再执行。想复用计算结果要另建缓存,不能把延后执行当成自动缓存,也不能假定 Lambda 捕获的对象可被多个线程安全共用。
代码先调用谁,再调用谁
- 调用者先创建 Supplier 行为对象
orElseGet检查 Optional 内部值- 存在则直接返回
- 缺失才执行
supplier.get并返回其结果
04动手写一个例子
按优先级惰性查找首个可用结果
输入按优先级排列的字符串提供者,每个返回 Optional<String>。依次执行直到首个存在值;全部缺失或列表为空返回空。只检查实际执行的提供者,异常直接传播,不吞成缺失。
- 输入 1
[空,值A,值B]- 输出 1
A,仅调用前两项
- 输入 2
[值A,会抛异常的提供者]- 输出 2
A,后者不执行
- 输入 3
[]- 输出 3
空
查找状态只有当前位置与当前返回值,不需要把所有提供者先执行再过滤。逐个执行后立即检查 isPresent,命中就返回,保证后续昂贵或有副作用的调用不会发生。这个短路规则不仅影响性能,也是外部行为数量的承诺。
提供者返回 null 容器属于接口实现错误,与返回 Optional.empty 的正常缺失不同,必须明确拒绝。提供者自身为 null 也只在轮到它时拒绝;成功之后的未访问项不会被检查。若调用者希望预先校验整个列表,可在配置加载阶段另做结构验证,不能在查找阶段悄悄破坏惰性。
测试使用调用计数器验证执行次数,不能只比较最终字符串。一个错误实现可能执行全部提供者却仍返回正确第一值,普通输出断言发现不了额外副作用。再加入抛异常提供者验证错误不会被吞掉,避免把数据库失败与确实查无数据混成同一个空结果。
为什么这样写是对的执行第 i 项前,前面所有已执行提供者都返回正常缺失;一旦得到存在值就终止。
每项只检查和执行一次,命中后不再遍历;调用内部可能包含昂贵工作,因此总成本必须累计提供者真实耗时。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
public class Main {
static Optional<String> first(List<Supplier<Optional<String>>> providers) {
if (providers == null) throw new IllegalArgumentException("providers");
for (Supplier<Optional<String>> provider : providers) {
if (provider == null) throw new IllegalArgumentException("provider");
Optional<String> value = provider.get();
// null 容器是实现错误,empty 才是正常缺失。
if (value == null) throw new IllegalStateException("null Optional");
if (value.isPresent()) return value; // 命中立即短路。
}
return Optional.empty();
}
static Supplier<Optional<String>> source(int[] calls, String value) {
return () -> { calls[0]++; return Optional.ofNullable(value); };
}
static void require(boolean condition) {
if (!condition) throw new AssertionError();
}
public static void main(String[] args) {
// 计数器只供单线程测试,验证结果之外的执行次数。
int[] calls = {0};
require(first(List.of(source(calls,"A"),source(calls,"B"))).equals(Optional.of("A")));
require(calls[0] == 1);
calls[0] = 0;
require(first(List.of(source(calls,null),source(calls,"B"))).equals(Optional.of("B")));
require(calls[0] == 2);
calls[0] = 0;
require(first(List.of(source(calls,null),source(calls,null))).isEmpty());
require(calls[0] == 2);
require(first(List.of()).isEmpty());
Supplier<Optional<String>> broken = () -> { throw new IllegalArgumentException("failure"); };
require(first(List.of(() -> Optional.of("A"),broken)).isPresent());
boolean propagated = false;
try { first(List.of(broken)); } catch (IllegalArgumentException e) { propagated = true; }
require(propagated);
boolean rejected = false;
try { first(List.of(() -> null)); } catch (IllegalStateException e) { rejected = true; }
require(rejected);
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空列表返回正常缺失。
- 已有结果时不执行后续错误提供者。
nullOptional 不等于 empty。- 提供者异常直接传播。
进阶选读 · 可选深入:并行竞速与结果缓存
场景多个来源延迟不同,希望最快可用结果或复用上次结果。
做法先决定优先级是否仍然严格,再设计并发竞速、取消、超时和缓存失效;记忆化还需区分空结果与错误是否缓存。
复杂度并行可降低等待但增加同时调用数量,缓存查询与刷新成本需分别计算。
好处与代价最先完成不等于最高优先级,取消也不保证外部副作用没有发生。
05项目里怎么用
Optional 适合表示方法可能没有结果;是否放进字段、参数或序列化对象,要看接口和框架支持。不要只是为了消除所有 null 到处加包装,也不要把空结果一概当成正常。查无数据、权限拒绝、网络故障和数据损坏应能区分,否则调用方不知道该提示、重试还是停止。
只有缺少结果时才计算备用值,适合缓存未命中后查原始数据、找备用配置等场景。但读取多次空结果,可能就会调用多次 Supplier。如果备用动作会发通知或创建记录,要决定是否只允许调用一次,或者怎样让重复调用不产生重复效果,这就是幂等问题。长期把 Supplier 存在字段里,也可能把它用到的大对象一直留在内存中;要检查它究竟保留了哪些对象。
函数式链条过长会隐藏中间业务含义和异常位置。适当把关键步骤命名成方法,保留输入校验和结果边界,比一味压成单行更易调试。并行执行前还要审查捕获对象是否线程安全;局部引用满足事实上的 final,只解决语言捕获限制,不等于对象内部状态没有竞争。
容易踩的坑
- 用
orElse执行昂贵或有副作用的默认计算。 - 把事实上的
final误解为捕获对象不可变。 - 把所有异常捕获后转成
Optional.empty。 - 认为惰性求值会自动缓存结果。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 直接返回值或抛异常 | 契约直接 | 正常缺失需要额外表示 | 结果必须存在的操作 |
| Optional 返回 | 明确表达正常缺失 | 容易被误用为错误掩盖 | 查询与可选配置结果 |
| Supplier 延迟行为 | 可控制执行时机与次数 | 可能隐藏副作用和资源引用 | 昂贵默认值与按需回源 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么 orElse 的默认方法即使不用也执行?
展开参考答案与推导
普通实参表达式在调用方法前求值,因此默认计算已经发生。orElseGet 接收 Supplier 行为,把调用位置放进缺失分支,才能实现按需执行。
02Lambda 捕获列表后还能 add,为什么?
展开参考答案与推导
捕获限制针对局部变量是否重新赋值,add 修改的是它指向的列表对象。引用保持不变不等于对象不可变,也不提供多个线程同时修改时的同步保证。
03map 与 flatMap 如何选择?
展开参考答案与推导
转换函数返回普通值时用 map;函数已经返回 Optional 时用 flatMap,避免多包一层。二者对于 null 返回还有不同约定,应让转换函数明确表达正常缺失而非返回混乱容器。
换个情况,还会处理吗
01缓存命中但数据库仍被查了,先检查什么?
展开参考答案与推导
检查是否写成 cached.orElse(loadFromDatabase()),使查询提前执行。改为惰性提供者并用调用次数测试命中路径,同时确认其它日志或参数构建没有提前触发同一查询。
02接口把网络错误转成空,调用者误以为不存在,怎么办?
展开参考答案与推导
保留错误与正常缺失的不同结果语义,通过异常或明确结果类型传递失败。Optional 本身只表达有没有值,不能承载所有失败原因,更不能自动决定重试策略。
03调用两次 orElseGet 为什么创建了两条记录?
展开参考答案与推导
惰性不是记忆化,每次缺失调用都可能执行提供者。创建动作需要幂等或显式缓存与事务控制,不应依赖 Optional 帮忙保证只执行一次。
写代码试一试
01增加后续 null 提供者的短路测试
- 输入 / 条件
[值A,null]- 输出 / 预期
A
提示 使用允许 null 的列表构造。
展开参考答案与推导
用 Arrays.asList 构造成功提供者和 null 项,断言 first 返回 A。List.of 会提前拒绝 null,无法测试算法的未访问项契约;因此测试准备方式本身也需要正确。
02增加空字符串结果测试
- 输入 / 条件
[Optional.of(空字符串),值B]- 输出 / 预期
空字符串
提示 存在与非空白不是一回事。
展开参考答案与推导
把第一项设为 Optional.of(""),应立即返回。若业务把空白视为缺失,应在提供者或明确 filter 步骤处理,不能把 Optional 存在判断偷偷改为字符串长度判断。
03把缺失转换成明确异常
- 输入 / 条件
所有提供者均空- 输出 / 预期
抛指定业务异常
提示 在最终结果调用 orElseThrow。
展开参考答案与推导
使用 first(providers).orElseThrow(() -> new IllegalStateException("not found"))。这表示该调用场景要求结果存在,提供者异常仍原样传播,不应与最终未找到结果混淆。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能把判断和转换作为参数传递,并显式处理可缺失结果。
本篇重点加练
重点练捕获变量与延迟默认值,检验回调行为是否受到时间和副作用影响。
- effectively
final捕获 - map
/filter/orElseGet
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
把“判断课程是否及格”“课程转标题”“输出日志”分别建模为函数式接口,写出 Lambda 的目标类型和参数返回类型;添加一个非函数式接口反例。
- 提交内容
- 接口表、可编译样例与反例编译记录。
- 怎样算完成
- 单抽象方法要求清楚
- 行为职责与接口类型对应
练到的知识点函数式接口单抽象方法 · Predicate/Function 等职责 · Lambda 目标类型
- 作业 02代码推演
针对
String::length、System.out::println 与捕获局部阈值的 Lambda,推导参数匹配;分别尝试修改被捕获变量和被捕获对象的字段后编译运行。- 提交内容
- 目标类型推演、编译结果与观察记录。
- 怎样算完成
- 绑定限制和对象可变性分开
- 方法引用由上下文核验
练到的知识点方法引用匹配 · effectively final 捕获
- 作业 03代码推演
两个 Lambda 捕获同一个配置对象,另一个捕获独立对象;构造后再修改一个字段,先画引用关系再观察回调,不能只比较变量是否
final。- 提交内容
- 对象关系图、预测与实际记录。
- 怎样算完成
- 捕获对象身份可区分
- 观察时刻明确
练到的知识点effectively final 捕获
综合应用
- 作业 04设计评审
用户昵称可能缺失,需要先过滤空白、转换展示格式,最后按需加载默认昵称;定义缺失与加载失败的合同,并选择 Optional 操作组织接口。
- 提交内容
- 处理流程、失败合同和调用样例。
- 怎样算完成
- 缺失不会与正常空字符串混同
- 默认值计算时机可验证
练到的知识点Optional 存在与缺失 · map/filter/orElseGet
- 作业 05编程实践
实现课程查询函数返回
Optional<Course>,再组合筛选和标题提取;让默认标题提供者统计调用次数,验证命中、未命中和不满足条件三类输入。- 提交内容
- Java 实现、自写边界和调用次数测试、运行证据。
- 怎样算完成
- 无无条件 get
- 默认提供者的执行符合合同
- 输入集合保持不变
练到的知识点Predicate/Function 等职责 · Optional 存在与缺失 · map/filter/orElseGet
- 作业 06面试场景
回调捕获同一个可变统计对象,被多个任务并发调用。评审“局部引用 effectively
final所以线程安全”的推理,设计需要补充的所有权证据。- 提交内容
- 对象关系图、并发场景与改进合同。
- 怎样算完成
- 捕获规则不代替同步规则
- 副作用范围明确
练到的知识点effectively final 捕获 · 副作用与共享对象
- 作业 07编程实践
实现可选学习目标的展示函数,默认值提供者读取一个可控计数对象;创建回调后再改变对象状态,分别在命中与缺失时调用并记录读取时点。
- 提交内容
- Java 实现、自写调用时序/空值测试及运行证据。
- 怎样算完成
- 创建回调与执行回调分开
- 默认值调用次数可核验
练到的知识点effectively final 捕获 · map/filter/orElseGet
- 作业 08设计评审
默认昵称需要一次可能失败的本地加载,团队同时使用
orElse和orElseGet。定义加载时机、失败传播和重复调用合同,再选择符合要求的接口。- 提交内容
- 两种调用的验收矩阵和理由。
- 怎样算完成
- 选择由可观察行为支持
- 缺失与加载失败分开
练到的知识点map/filter/orElseGet
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成泛型后:实现可组合验证规则接口,支持先后验证和汇总错误两种政策;规则不得静默修改输入,测试短路与全部执行的调用轨迹。
- 提交内容
- Java 实现、自写空规则/失败顺序/副作用测试及运行证据。
- 怎样算完成
- 组合政策清楚
- 每条错误能关联规则
- 函数行为可重复测试
练到的知识点函数式接口单抽象方法 · Predicate/Function 等职责 · map/filter/orElseGet · 副作用与共享对象
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
回调从“定义后立即调用一次”改为“保存到队列一小时后可能多次调用”。重新审查配置捕获、默认结果新鲜度与副作用次数。
- 提交内容
- 延迟调用时间线和合同变更表。
- 怎样算完成
- 旧时间假设得到明确审查
- 样本能区分一次与多次执行
练到的知识点effectively final 捕获 · map/filter/orElseGet
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Dev.java:Lambda 表达式 (新标签页)↗
阅读函数式接口、目标类型和方法引用,连接普通方法与 Stream 参数。
- 实践参考文档英文
Java 17 API:Optional (新标签页)↗
练习 map、
flatMap、orElseGet与空结果,避免无条件 get。 - 深入规范英文
Java 17 语言规范:表达式 (新标签页)↗
查阅运算符、求值顺序、短路、方法调用与 Lambda 的目标类型规则。
- 深入参考文档英文
OpenJDK 17u java.util.Optional (新标签页)↗
定位
orElseGet的存在值分支与supplier.get调用时机。 - 深入参考文档英文
Java17 函数接口包 (新标签页)↗
阅读 Predicate、Function、Consumer、Supplier 的输入输出形状。