qinghuan
← Java SE / 全部笔记

单元 54 / Java SE

P0 · 必会主线L2 · 进阶

Stream:筛选、转换和汇总一批数据

用筛选、转换和分组处理一批数据,理解什么时候真正执行,以及收集结果和并行处理要遵守什么规则。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能写出清楚的 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["不可修改结果列表"]

按顺序看,每一步做了什么

  1. 写清来源、元素类型与终止结果。
  2. 区分无状态、有状态和短路阶段。
  3. 为重复键与平价情况定义规则。
  4. 证明归约与组合满足必要性质。
  5. 分别测试结果、可变性和并行一致性。

03对照实现代码

默认 toMap 为什么拒绝重复键 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/util/stream/Collectors.java · java.util.stream.Collectors#toMap(Function,Function)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
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 并不自动包含所有保证。

代码先调用谁,再调用谁

  1. toMap 创建 Collector
  2. supplier 建立分段 Map
  3. accumulator 映射键值并拒绝重复
  4. combiner 合并分段并继续维护唯一键规则
  5. 终止操作交付结果

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,每条记录的映射操作不是无条件常数。

为什么这样写是对的每个部分映射中,每组保存当前已消费记录按分数降序、用户名升序规则选出的唯一最佳值。

时间 顺序实现 O(n log g) 上界,g 为不同组数;并行合并有额外成本。额外空间 O(g) 顺序累积与输出;并行分段可能使用更多临时映射。

每条记录进行 TreeMap 键定位并作常数个字段比较;用户名和组名比较还与字符长度有关。并行分段并不改变必须处理全部记录的事实。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 47 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 三篇,保留各篇已有内容。

固定数据与步骤

  1. 自建两条虚构记录:1001 属于租户 A,名为“测试茶铺”,营业时间 09:00–18:00;1002 属于租户 B,名称和营业时间保持空白,不补出未设定信息。仅在本节内存排序练习中,为两条记录附加演示评分 4.5;它不是现实评价,也不进入后续 REST/Agent 的基线字段。
  2. 在集合篇使用 List<Merchant> 保存待处理记录,使用 Map<Long, Merchant> 按编号查找。插入重复编号前先写清规则:本练习拒绝重复编号,不能默认后插入者覆盖前者。
  3. 在泛型篇把查询结果包装成 LookupResult<Merchant>,明确“找到”和“不存在”的状态;不要把 Object 强转成功当成类型安全。构造一次把字符串放入 List<Merchant> 的错误写法,记录编译器如何拦住它。
  4. 在 Stream 篇先用循环,再用 filter 和 sorted 完成同一任务:筛选评分至少 4.0 的商户,按评分降序、编号升序排序,最后收集成列表。筛选阈值没有命中时返回空列表。
  5. 正式进入 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 与归约
  • 并行与共享副作用

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    对 [3,1,3,2] 建立带 filter、map 和终止操作的流水线,加入受控计数记录;先预测何时开始求值,再尝试再次使用同一流水线并独立验证。

    提交内容
    操作阶段图、最小代码与执行记录。
    怎样算完成
    • 区分建立流水线与消费
    • 一次消费边界有证据

    练到的知识点数据源和流水线 · 中间与终止操作 · 一次消费

  2. 作业 02原理问答

    输入学生列表,每人有多个标签且标签可能重复。分别描述 map 与 flatMap 后的数据形状,并说明 distinct 应依据哪一层对象的相等合同。

    提交内容
    转换前后类型图和样本设计。
    怎样算完成
    • 嵌套层次清楚
    • 去重对象和比较规则明确

    练到的知识点map 与 flatMap · distinct 相等规则

综合应用

  1. 作业 03编程实践

    统计每门课程的有效成绩人数与总分,输入含同名学生、重复记录及缺考标记;先定义记录去重和缺考政策,再用 Stream 生成结果。

    提交内容
    Java 实现、自写空输入/重复/缺考测试及运行证据。
    怎样算完成
    • 分组与业务身份不混淆
    • 归约结果可由独立循环核对
    • 说明数值范围与溢出策略

    练到的知识点map 与 flatMap · distinct 相等规则 · groupingBy 与归约

  2. 作业 04排错实验

    分别用 toList 收集结果以及向共享 ArrayList 写入并行流水线结果,记录修改和并发实验现象;为两段实现编写符合明确合同的替代版本。

    提交内容
    失败样例、自写边界和重复运行测试、修复证据。
    怎样算完成
    • 区分容器修改限制与元素可变性
    • 并发结论不只依赖一次运行

    练到的知识点toList 不可修改 · 并行与共享副作用

  3. 作业 05面试场景

    日志处理流水线在 peek 内保存数据库记录,后续终止操作改为短路查询。评审副作用的业务可靠性,明确计算结果与持久化动作的验收要求。

    提交内容
    风险时间线和接口重构提案。
    怎样算完成
    • 考虑短路和失败
    • 业务副作用可单独验证

    练到的知识点数据源和流水线 · 中间与终止操作 · groupingBy 与归约 · 并行与共享副作用

  4. 作业 06编程实践

    计算每位学员成绩的最小值、最大值与样本数,输入可能没有有效成绩;用同一数据检验循环与 Stream 版本的合同,不以哨兵分数表示缺失。

    提交内容
    Java 实现、自写空组/重复/极值测试与运行证据。
    怎样算完成
    • 空结果有明确类型或标记
    • 多个统计量使用同一有效样本集

    练到的知识点中间与终止操作 · groupingBy 与归约

  5. 作业 07排错实验

    给数据源映射阶段加入受控观察记录,分别使用短路查询、完整收集和只创建流水线三种流程;记录实际求值并说明证据适用范围。

    提交内容
    Java 实验、调用轨迹与测试记录。
    怎样算完成
    • 终止操作差异有可比输入
    • 日志观察不被当作普遍执行承诺

    练到的知识点中间与终止操作

  6. 作业 08设计评审

    订单归约需要总额、订单数与错误行数,某方案并行更新外部三个计数器。定义结果一致性合同,并制定能比较替代归约方案的测试。

    提交内容
    统计状态规范和分区验收矩阵。
    怎样算完成
    • 字段来自同一次处理合同
    • 数据分区不改变业务口径

    练到的知识点groupingBy 与归约 · 并行与共享副作用

  7. 作业 09面试场景

    流水线内调用远程服务,团队仅改为 parallel 后发现超时增加。写出并发量、外部配额、失败收集和结果次序的证据需求。

    提交内容
    负载图和排查顺序。
    怎样算完成
    • 并行度与外部限制分别评估
    • 结论不以 CPU 空闲作唯一依据

    练到的知识点并行与共享副作用

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    完成并行计算后:设计用于平均分统计的可组合中间状态,列出组合合同和测试方法,验证不同分区与合并顺序下的结果政策。

    提交内容
    归约状态规范、分区测试矩阵和数值误差要求。
    怎样算完成
    • 空分区语义明确
    • 组合性质可测试
    • 浮点与精确数值政策区分

    练到的知识点groupingBy 与归约 · 并行与共享副作用

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    检验“任何二元运算都能作为并行归约的组合函数”。选择一种涉及顺序或舍入的业务运算,给出不同分组下的检查过程。

    提交内容
    运算合同、分组样本和适用边界。
    怎样算完成
    • 比较保持输入集合相同
    • 论证涉及组合行为而非运行偶然性

    练到的知识点groupingBy 与归约 · 并行与共享副作用

  2. 作业 12思维拓展 · 条件变化

    统计源从内存列表改为每读取一项都会记账的外部迭代器。审查原来的短路、重跑与多次统计设计,制定可接受的数据消费合同。

    提交内容
    消费次数模型与替代接口方案。
    怎样算完成
    • 一次消费的业务成本明确
    • 外部副作用与统计正确性分别验证

    练到的知识点中间与终止操作 · groupingBy 与归约

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗