读完这篇,你能做什么
能写出清楚的 Stream 处理步骤,避免修改共享累加器,并说明结果集合是否可修改等约定。
学习理由、准备要求与知识点
为什么学它
Stream 常用于集合转换和统计,需要知道计算何时开始、结果如何收集,以及操作是否会修改共享数据。
先读这些内容
知识点清单
- 数据源和流水线
- 中间与终止操作
- 一次消费
- map 与
flatMap - distinct 相等规则
groupingBy与归约toList不可修改- 并行与共享副作用
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
Stream 用来描述“这批数据接下来怎样处理”,本身不是新的存储集合。来源提供元素,filter、map 等中间操作记录筛选和转换步骤,collect、reduce、findFirst 等终止操作才推动实际计算。一个流通常不能消费后再用;要再算一次,应从原来的数据创建新流。
常用操作可以按动作理解:filter 留下符合条件的项,map 把每项转换成一个结果,flatMap 把每项产生的多项结果展开。sorted 需要排序,distinct 需要去重,limit 和 findFirst 可能提前结束。处理顺序既影响结果也影响花费:先筛选再排序通常要排更少的数据,但只有不改变业务结果时才能调整次序。
收集器把元素累积到结果容器,并定义并行分段时如何合并。toMap 未提供冲突函数时,重复键会失败;有重复键就应明确取第一项、最后一项、汇总还是拒绝。Java17 Stream.toList 返回不可修改列表,Collectors.toList 不承诺特定可变性或实现类型,需要可写 ArrayList 时使用明确的 toCollection。
文中这几个词,先认一认
- 惰性中间操作
- 先记录要做的处理,等终止操作需要数据时才实际执行。
- 短路
- 结果已经足够时可以提前停止,不必读取所有输入,例如找到第一项就结束。
- 结合律
- 把一组数据换个括号分组,合并结果仍相同;并行拆开计算再合并常依赖这个性质。
- 遇到顺序
- 比如列表按甲、乙、丙排列,它产生的有序流就带着这套先后关系,某些操作也会规定或改变次序。这叫遇到顺序,不代表并行时处理甲的线程一定先运行或先完成。
02看看它怎么工作
流水线可以融合多个无状态阶段,使元素按需经过过滤与映射,而非每一步都创建完整中间列表。但有状态操作可能需要缓冲,排序通常不能只看第一个元素就完成。实现还可以省略对最终结果无影响的阶段,因此不要依赖 peek 或带副作用的 map 恰好执行某个次数来完成业务动作。
reduce 和 collect 的组合规则必须与目标一致。加法对固定范围整数可结合,但浮点加法因舍入不严格满足数学结合律;求平均也不能把两个平均值直接再平均而忽略样本数。并行收集通常给不同分段独立容器,再用 combiner 合并,不等于多个线程安全地共享一个普通 ArrayList 随意 add。
要分开检查结果顺序、能否修改和线程执行方式。并行流的 forEach 不保证按来源中的先后处理;forEachOrdered 可以让输出动作遵守这个顺序,但前面的计算仍可能并行。下面的程序先给出完整比较规则,让每组总能选出明确的最佳值,再用 TreeMap 固定各组的输出顺序。这样即使分段结果以不同先后合并,也不应改变最终答案,而不是依赖某次运行恰好输出得整齐。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["来源记录"] --> B["校验与无状态转换"]
B --> C["按组映射为候选值"]
C --> D["重复键用确定的 best 合并"]
D --> E["并行分段也按同一规则组合"]
E --> F["TreeMap 固定组顺序"]
F --> G["不可修改结果列表"]按顺序看,每一步做了什么
- 写清来源、元素类型与终止结果。
- 区分无状态、有状态和短路阶段。
- 为重复键与平价情况定义规则。
- 证明归约与组合满足必要性质。
- 分别测试结果、可变性和并行一致性。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/util/stream/Collectors.java · java.util.stream.Collectors#toMap(Function,Function)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public final class SourceModel {
public static <T,K,V> Map<K,V> uniqueMap(Iterable<T> source,
Function<? super T,? extends K> keys,
Function<? super T,? extends V> values) {
Map<K,V> result = new HashMap<>();
for (T item : source) {
K key = keys.apply(item);
V value = java.util.Objects.requireNonNull(values.apply(item));
// 即使两个映射值相等,重复键仍违反唯一键契约。
if (result.putIfAbsent(key,value) != null)
throw new IllegalStateException("duplicate key");
}
return result;
}
}真实双参数 Collectors.toMap 创建使用 HashMap 的收集器,并配置唯一键累积与合并逻辑。模型用普通循环展示单段累积,拒绝 null 映射值并在重复键时失败,不实现 Collector 的供应器、组合器与并行框架。它说明默认行为为何要求键唯一,而不是完整复制 Stream 执行器。
重复键检查关注的是键的相等规则,即使两次映射得到完全相同的值,仍可能被视为重复键错误。若业务允许重复,需要选带合并函数的重载,并写出合并含义。静默覆盖会丢失信息,默认拒绝能迫使调用者面对这项选择,但它不是所有业务最合适的最终策略。
并行归约还要处理不同分段结果之间的键冲突,不能只在单个累积器里检查。一个分段内部没有重复,不代表另一个分段不会产生同键。真实收集器把累积与组合规则配套定义,教学模型只演示串行局部,因此不应用作自行并发共享的收集器。
返回类型写 Map,只表示可以按 Map 接口使用,不能单凭它推断遍历顺序、线程安全或以后一定允许修改。需要 TreeMap、LinkedHashMap 或不可修改结果时,要明确选择创建方式或后续转换。分别核对类型、重复键处理和结果容器的属性,collect 并不自动包含所有保证。
代码先调用谁,再调用谁
toMap创建 Collector- supplier 建立分段
Map - accumulator 映射键值并拒绝重复
- combiner 合并分段并继续维护唯一键规则
- 终止操作交付结果
04动手写一个例子
按组选择最高分并稳定处理平分
输入 Score(group,user,points),每组选最高分;平分时选择用户名按 String.compareTo 更小者,输出按组名升序。计算结果在顺序流与并行流中应一致,拒绝空组件和分数越界。
- 输入 1
A组:lin90、an90;B组:bo80- 输出 1
A组an90,B组bo80
- 输入 2
[]- 输出 2
[]
- 输入 3
同组同用户重复记录- 输出 3
保留一个相同最佳值
用 toMap 将组名作为键,候选记录作为值。合并函数先比较分数,再比较用户名,等价于在一个明确全序下取最大优先项;无论先比较哪一对、怎样分组,最终都选出同一最佳值。把平分规则写进函数,避免并行合并顺序决定业务结果。
TreeMap 供应器让最终组顺序按字符串自然顺序排列。这个排序仅是教学规则,不等于所有地区的人名排序方式。返回 List.copyOf 的值快照使调用者不能改变结果列表结构,Score 的字符串与 int 组件也构成稳定值,因此这里的浅复制足以满足示例要求。
顺序与并行测试比较完整结果而非仅比较数量。小样本并行一致不能证明性能更快,但能够检验给定用例中的组合语义。真实性能还取决于来源拆分、元素数量、比较和合并成本;此算法使用 TreeMap,每条记录的映射操作不是无条件常数。
为什么这样写是对的每个部分映射中,每组保存当前已消费记录按分数降序、用户名升序规则选出的唯一最佳值。
每条记录进行 TreeMap 键定位并作常数个字段比较;用户名和组名比较还与字符长度有关。并行分段并不改变必须处理全部记录的事实。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
record Score(String group, String user, int points) {
Score {
if (group == null || group.isBlank() || user == null || user.isBlank()
|| points < 0 || points > 100) throw new IllegalArgumentException("score");
}
}
static Score better(Score a, Score b) {
if (a.points() != b.points()) return a.points() > b.points() ? a : b;
// 平分规则明确,不能交给并行合并先后决定。
return a.user().compareTo(b.user()) <= 0 ? a : b;
}
static List<Score> best(List<Score> input, boolean parallel) {
if (input == null || input.stream().anyMatch(x -> x == null))
throw new IllegalArgumentException("input");
Stream<Score> source = parallel ? input.parallelStream() : input.stream();
Map<String,Score> result = source.collect(Collectors.toMap(
Score::group, Function.identity(), Main::better, TreeMap::new));
return List.copyOf(result.values()); // 明确不可修改的结果结构。
}
static void require(boolean condition) {
if (!condition) throw new AssertionError();
}
public static void main(String[] args) {
Score lin = new Score("A","lin",90), an = new Score("A","an",90);
Score bo = new Score("B","bo",80), low = new Score("A","z",50);
List<Score> input = List.of(bo,lin,low,an);
// 组顺序、平分、空输入和顺并行结果逐项比较。
require(best(input,false).equals(List.of(an,bo)));
require(best(input,true).equals(best(input,false)));
require(best(List.of(),false).isEmpty());
require(best(List.of(an,an),true).equals(List.of(an)));
boolean invalid = false;
try { new Score("A","x",101); } catch (IllegalArgumentException e) { invalid = true; }
require(invalid);
boolean immutable = false;
try { best(input,false).add(lin); } catch (UnsupportedOperationException e) { immutable = true; }
require(immutable);
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 空输入返回不可修改空列表。
- 同分不同用户有明确平价规则。
- 重复同一最佳记录不改变结果。
- 并行模式不允许共享输入同时被修改。
进阶选读 · 可选深入:平均值与自定义 Collector
场景每组需要平均、方差或多个统计量,且希望并行计算。
做法设计足够统计状态,例如计数与总和,并给出正确组合公式;对浮点误差、溢出及 Collector 特征逐一说明,不能只平均分段平均值。
复杂度通常每项常数统计工作,分组映射和组合成本另计。
好处与代价自定义收集器更高效但证明负担更大,先用清晰循环建立正确参考结果。
05项目里怎么用
Stream 适合表达数据转换和聚合,涉及复杂控制流、多个早退原因或细致资源处理时,普通循环可能更清楚。不要为了函数式外观把副作用藏进 peek 或 map,尤其是数据库写入和外部调用;优化可能省略阶段,异常中断也可能留下部分副作用。
并行流共享执行资源并增加拆分、调度与合并开销,不能只加 parallel 就宣布吞吐提升。阻塞 IO、共享可变状态和数据规模过小都可能使效果变差;必须在真实工作负载下测量,并与明确线程池的执行方案比较资源隔离需求。
来源是 IO 的流还要负责关闭,例如 Files.lines 应放在合适的 try-with-resources 中。终止操作做完,不代表所有来源都会自动关闭;不能照搬遍历普通集合的经验。返回结果时也应说明列表能不能改、元素会不会变,以及是否保证顺序。
容易踩的坑
- 没有终止操作却期待流水线执行。
- 把 peek 或 map 副作用当作必然发生的业务动作。
toMap重复键时未定义合并规则。- 认为 parallel 自动更快且普通共享容器也安全。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 普通循环 | 控制流、异常和副作用直观 | 聚合样板可能较多 | 复杂步骤与需要精细诊断的处理 |
| 顺序 Stream | 转换与归约意图集中 | 过长链条可能难调试 | 纯数据映射和聚合 |
| 并行 Stream | 可利用可分割 CPU 工作 | 调度、合并和共享资源成本 | 足够大且无共享副作用的计算 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01Stream 与集合有什么不同?
展开参考答案与推导
集合保存元素并提供容器操作,Stream 描述一次计算流程并按终止需求消费来源。流通常不可重复使用,想再次计算应重建流,而不是像列表一样保存后反复遍历同一实例。
02为什么重复键需要合并函数?
展开参考答案与推导
同一键对应多个候选值时没有唯一默认业务答案。拒绝、求和、保留首项或选择最佳都可能合理,合并函数把规则显式写出,并且并行组合也必须遵循它。
03为什么两个平均值不能直接再平均?
展开参考答案与推导
分段样本数量可能不同,直接平均会给各段相同权重。应保留每段总和与数量并合并,再求最终平均;统计状态必须足够支持正确组合。
换个情况,还会处理吗
01count 没有执行 peek 日志,是库错误吗?
展开参考答案与推导
不一定。实现可省略不影响终止结果的阶段,带副作用的观察不能作为业务保证。需要必然执行的动作应放在明确终止消费或普通循环中,并处理异常和资源边界。
02并行收集出现少数据,先检查什么?
展开参考答案与推导
查看是否多个线程向同一个普通 ArrayList 等可变容器写入,或组合器遗漏部分状态。使用符合 Collector 契约的独立累积与组合方式,再验证输入是否也被并发修改。
03换 Stream.toList 后 add 失败,为什么?
展开参考答案与推导
Java17 Stream.toList 返回不可修改列表。需要可写容器时显式使用 Collectors.toCollection(ArrayList::new),不要依赖其它收集方法当前碰巧返回某个可变实现。
写代码试一试
01把平分规则改为用户名较大者
- 输入 / 条件
A组an90、lin90- 输出 / 预期
lin90
提示 修改比较方向并同步测试。
展开参考答案与推导
在 better 的同分分支选择 compareTo 更大者,顺序与并行测试都应更新。它仍是明确全序下的选择,但业务语义改变,不能只把旧预期留给执行顺序决定。
02验证输入顺序变化不改变最佳值
- 输入 / 条件
将输入列表反转- 输出 / 预期
同样的组最佳结果
提示 合并规则与到达顺序无关。
展开参考答案与推导
复制并反转输入后比较 best 结果。该性质依赖本题按分数和用户名定优先级;若需求是保留第一条记录,则反转后变化反而可能是正确行为。
03实现每组平均分
- 输入 / 条件
A组80、100- 输出 / 预期
90
提示 保存总和与计数。
展开参考答案与推导
可以收集到 sum 与 count 的状态再相除,或使用符合需求的 averagingInt。若必须精确十进制与指定舍入,应明确结果类型和规则,不把 double 平均直接用于金额结算。
商户项目 · 本篇加练
用集合与 Stream 筛选商户
核查日期:2026-10-11。状态:本节为新设计的教学练习,尚未编译或运行;下列期望不是测试结果。
目标与前置知识
目标是把同一批商户按规则筛选、排序和按编号查找,并解释每个容器的作用。先掌握类、方法、条件判断、循环以及对象字段;按顺序读本站的集合、泛型、Stream 三篇,保留各篇已有内容。
固定数据与步骤
- 自建两条虚构记录:1001 属于租户 A,名为“测试茶铺”,营业时间 09:00–18:00;1002 属于租户 B,名称和营业时间保持空白,不补出未设定信息。仅在本节内存排序练习中,为两条记录附加演示评分 4.5;它不是现实评价,也不进入后续 REST/Agent 的基线字段。
- 在集合篇使用
List<Merchant>保存待处理记录,使用Map<Long, Merchant>按编号查找。插入重复编号前先写清规则:本练习拒绝重复编号,不能默认后插入者覆盖前者。 - 在泛型篇把查询结果包装成
LookupResult<Merchant>,明确“找到”和“不存在”的状态;不要把Object强转成功当成类型安全。构造一次把字符串放入List<Merchant>的错误写法,记录编译器如何拦住它。 - 在 Stream 篇先用循环,再用
filter和sorted完成同一任务:筛选评分至少 4.0 的商户,按评分降序、编号升序排序,最后收集成列表。筛选阈值没有命中时返回空列表。 - 正式进入 Agent 路线前,把身份范围先限定好。租户 A 的输入数据只允许包含 1001,B 只允许包含 1002;不能先把所有商户交给模型,再期待提示词替你过滤。
验收与交付物
提交数据文件、循环版与 Stream 版、五组断言说明:普通数据、空列表、恰好等于阈值、评分相同、重复编号。两种实现应得到相同编号顺序;输入列表不能被意外改写。把实际输出、运行命令与 JDK 版本留空,运行后再填写,不把设计期望填成运行结果。
这一步只处理内存中的虚构数据,不证明数据库性能,也不引入并行 Stream。下一步接到只读 REST 商户查询。
原始来源与阅读范围
- dev.java 学习入口:学习路径入口,滚动文档;页脚当前指向 Java 27,不意味着本项目需要升级到 Java 27。
- 集合:先读 Storing Data、List、Set、Using Maps 三组内容。
- 泛型:先读 Introducing Generics;通配符和擦除放在能解释
List<Merchant>后再读。 - Stream:先读 Processing Data、Intermediate Operations、Terminal Operation;本练习不需要 Gatherer 或并行流。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能写出清楚的 Stream 处理步骤,避免修改共享累加器,并说明结果集合是否可修改等约定。
本篇重点加练
重点增加求值时机、归约合同和共享副作用练习,覆盖更换终止方式与并行策略后的语义变化。
- 中间与终止操作
groupingBy与归约- 并行与共享副作用
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
对 [3,1,3,2] 建立带 filter、map 和终止操作的流水线,加入受控计数记录;先预测何时开始求值,再尝试再次使用同一流水线并独立验证。
- 提交内容
- 操作阶段图、最小代码与执行记录。
- 怎样算完成
- 区分建立流水线与消费
- 一次消费边界有证据
练到的知识点数据源和流水线 · 中间与终止操作 · 一次消费
- 作业 02原理问答
输入学生列表,每人有多个标签且标签可能重复。分别描述 map 与
flatMap后的数据形状,并说明 distinct 应依据哪一层对象的相等合同。- 提交内容
- 转换前后类型图和样本设计。
- 怎样算完成
- 嵌套层次清楚
- 去重对象和比较规则明确
练到的知识点map 与 flatMap · distinct 相等规则
综合应用
- 作业 03编程实践
统计每门课程的有效成绩人数与总分,输入含同名学生、重复记录及缺考标记;先定义记录去重和缺考政策,再用 Stream 生成结果。
- 提交内容
- Java 实现、自写空输入/重复/缺考测试及运行证据。
- 怎样算完成
- 分组与业务身份不混淆
- 归约结果可由独立循环核对
- 说明数值范围与溢出策略
练到的知识点map 与 flatMap · distinct 相等规则 · groupingBy 与归约
- 作业 04排错实验
分别用
toList收集结果以及向共享ArrayList写入并行流水线结果,记录修改和并发实验现象;为两段实现编写符合明确合同的替代版本。- 提交内容
- 失败样例、自写边界和重复运行测试、修复证据。
- 怎样算完成
- 区分容器修改限制与元素可变性
- 并发结论不只依赖一次运行
练到的知识点toList 不可修改 · 并行与共享副作用
- 作业 05面试场景
日志处理流水线在 peek 内保存数据库记录,后续终止操作改为短路查询。评审副作用的业务可靠性,明确计算结果与持久化动作的验收要求。
- 提交内容
- 风险时间线和接口重构提案。
- 怎样算完成
- 考虑短路和失败
- 业务副作用可单独验证
练到的知识点数据源和流水线 · 中间与终止操作 · groupingBy 与归约 · 并行与共享副作用
- 作业 06编程实践
计算每位学员成绩的最小值、最大值与样本数,输入可能没有有效成绩;用同一数据检验循环与 Stream 版本的合同,不以哨兵分数表示缺失。
- 提交内容
- Java 实现、自写空组/重复/极值测试与运行证据。
- 怎样算完成
- 空结果有明确类型或标记
- 多个统计量使用同一有效样本集
练到的知识点中间与终止操作 · groupingBy 与归约
- 作业 07排错实验
给数据源映射阶段加入受控观察记录,分别使用短路查询、完整收集和只创建流水线三种流程;记录实际求值并说明证据适用范围。
- 提交内容
- Java 实验、调用轨迹与测试记录。
- 怎样算完成
- 终止操作差异有可比输入
- 日志观察不被当作普遍执行承诺
练到的知识点中间与终止操作
- 作业 08设计评审
订单归约需要总额、订单数与错误行数,某方案并行更新外部三个计数器。定义结果一致性合同,并制定能比较替代归约方案的测试。
- 提交内容
- 统计状态规范和分区验收矩阵。
- 怎样算完成
- 字段来自同一次处理合同
- 数据分区不改变业务口径
练到的知识点groupingBy 与归约 · 并行与共享副作用
- 作业 09面试场景
流水线内调用远程服务,团队仅改为 parallel 后发现超时增加。写出并发量、外部配额、失败收集和结果次序的证据需求。
- 提交内容
- 负载图和排查顺序。
- 怎样算完成
- 并行度与外部限制分别评估
- 结论不以 CPU 空闲作唯一依据
练到的知识点并行与共享副作用
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
完成并行计算后:设计用于平均分统计的可组合中间状态,列出组合合同和测试方法,验证不同分区与合并顺序下的结果政策。
- 提交内容
- 归约状态规范、分区测试矩阵和数值误差要求。
- 怎样算完成
- 空分区语义明确
- 组合性质可测试
- 浮点与精确数值政策区分
练到的知识点groupingBy 与归约 · 并行与共享副作用
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
检验“任何二元运算都能作为并行归约的组合函数”。选择一种涉及顺序或舍入的业务运算,给出不同分组下的检查过程。
- 提交内容
- 运算合同、分组样本和适用边界。
- 怎样算完成
- 比较保持输入集合相同
- 论证涉及组合行为而非运行偶然性
练到的知识点groupingBy 与归约 · 并行与共享副作用
- 作业 12思维拓展 · 条件变化
统计源从内存列表改为每读取一项都会记账的外部迭代器。审查原来的短路、重跑与多次统计设计,制定可接受的数据消费合同。
- 提交内容
- 消费次数模型与替代接口方案。
- 怎样算完成
- 一次消费的业务成本明确
- 外部副作用与统计正确性分别验证
练到的知识点中间与终止操作 · groupingBy 与归约
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Java 17 API:Stream (新标签页)↗
核对惰性、一次消费、无干扰操作和
toList的不可修改语义。 - 实践参考文档英文
Java 17 API:Collectors (新标签页)↗
练习分组、映射、归约和重复键合并,明确收集结果结构。
- 深入教程英文
Dev.java:Lambda 表达式 (新标签页)↗
阅读函数式接口、目标类型和方法引用,连接普通方法与 Stream 参数。
- 深入参考文档英文
OpenJDK 17u java.util.stream.Collectors (新标签页)↗
定位双参数
toMap的唯一键累积与组合器,说明默认重复键失败及模型只覆盖串行部分。 - 深入参考文档英文
Java17 Stream 包说明 (新标签页)↗
阅读惰性、干扰、副作用、有状态操作与并行归约。
- 深入参考文档英文
Java17 Collector API (新标签页)↗
学习 supplier、accumulator、combiner 及结合性约束。