20260804-笔记.md 51 KB

20260804 课堂笔记 — 函数式编程与 Stream 流

  • 日期:2026-08-04
  • 项目c260804
  • 包路径course / homework0803
  • 作者:WanJL

目录

  1. 函数式编程思想:概念与特点
  2. Lambda 表达式回顾(JDK8 语法糖)
  3. Stream 流入门案例:筛选"万"开头 + 长度为 3
  4. Stream 流的思想:三类方法(获取 / 中间 / 终结)
  5. Stream 流中间操作方法:filter / limit / skip / map / concat / distinct
  6. 方法引用(Method Reference):四种形式
  7. Stream 流终结操作方法:forEach / count
  8. Stream 流收集操作方法:collect + Collectors(toList / toSet / toMap)
  9. 生成 Stream 流的四种方式
  10. 20260803 课后作业回顾:Lambda 表达式应用(Addable / Flyable)
  11. 20260803 课后作业回顾:Employee 员工管理(TreeSet 自然排序 + Comparator 按姓名 + 方法引用)
  12. 随堂练习要点
  13. 拓展阅读

1. 函数式编程思想:概念与特点

概念

函数式编程是一种编程范式,核心思想就是把计算看作是数学中的函数计算——程序由一些数学函数的组合构成,通过组合这些函数来完成计算任务;而不是像命令式那样写一条一条的语句序列来达到目的。

// 来源:course/Demo01.java(注释部分)
/*
    函数式编程是一种编程范式,核心思想就是把计算看作是数学中的函数计算,
    在函数式编程的过程中,程序由一些数学函数的组合构成,通过组合这些函数来完成计算任务。
    而不是像命令式的语句序列一样来达到目的。
*/

函数式编程的五大特点

特点 说明
不可变性 变量只能被赋值一次,在其生命周期内保持不变,减少程序错误,更好维护
高阶函数 支持把函数作为参数传递,以及把函数作为返回值,可创建更通用的函数
函数组合 通过组合多个小的、独立的函数构建更大、更复杂的函数(核心思想)
无副作用 函数执行结果只取决于输入的参数,不会对函数外部环境产生影响
延迟求值 最终值只在必要的时候才被计算,提高程序执行效率

应用场景

一般用在并发编程、异步编程、数据处理以及各种机器学习和数据科学等方向中。

Java 中的体现

Java 中使用 Lambda 表达式就是一种函数式编程的体现,Lambda 属于 JDK8 的一种语法糖

// 来源:course/Demo01.java(注释部分)
(参数) -> {代码块}    // 这就是 Java 语法糖

关键理解

  1. 函数式编程 ≠ 新语言:Java 通过 Lambda 表达式把函数式编程"引进来",它是对匿名内部类的语法简化。
  2. 与面向对象的对比:面向对象强调"通过对象的形式做事情",函数式编程强调"做什么",忽略繁琐的"以什么形式去做"。
  3. Stream 流才是真正把函数式编程引入 Java(详见第 3~9 节)。

2. Lambda 表达式回顾(JDK8 语法糖)

昨天(0803)已经学习了 Lambda 的标准格式、省略模式、使用前提,今天是它的应用延续。

标准格式回顾

(形式参数) -> {代码块}

三要素:形式参数、箭头 ->、代码块。

使用前提(回顾)

  1. 必须要有接口——Lambda 的本质是简化接口的匿名实现
  2. 接口中有且只有一个抽象方法——即必须是函数式接口

与匿名内部类的区别(回顾)

对比维度 匿名内部类 Lambda 表达式
需要的类型 接口、抽象类、普通具体类均可 只能是接口
使用限制 多抽象方法也能用 必须只有一个抽象方法
实现原理 编译后产生独立 .class 文件 编译后不产生单独 .class 文件,运行期动态生成字节码

3. Stream 流入门案例:筛选"万"开头 + 长度为 3

案例需求

按照下面的要求完成集合的创建和遍历:

  1. 创建一个集合,存储多个字符串元素
  2. 把集合中所有以 "万"开头 的元素存储到一个新的集合
  3. 把"万"开头的集合中长度为 3 的元素存储到一个新的集合
  4. 遍历上一步得到的集合

传统写法(以前学过的写法)

// 来源:course/Demo01.java(注释部分:传统 for-each 写法)
ArrayList<String> list = new ArrayList<>();
list.add("万震山");
list.add("万梓良");
list.add("万事如意");
list.add("王老五");
list.add("万巨龙");
list.add("张三");

// - 把集合中所有以"万"开头的元素存储到一个新的集合
ArrayList<String> list02 = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("万")) {
        list02.add(s);
    }
}

// - 把"万"开头的集合中的长度为3的元素存储到一个新的集合
ArrayList<String> list03 = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("万") && s.length() == 3) {
        list03.add(s);
    }
}

// - 遍历上一步得到的集合
System.out.println(list03);

缺点:要写好几个循环 + 好几个中间集合,代码繁琐、语义不直观。

Stream 流写法

// 来源:course/Demo01.java(注释部分:Stream 写法)
ArrayList<String> list = new ArrayList<>(List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三"));

// Stream流 - 把集合中所有以"万"开头的元素存储到一个新的集合
list.stream().filter(s -> s.startsWith("万")).forEach(System.out::println);

// Stream流 - 把"万"开头的集合中的长度为3的元素存储到一个新的集合
list.stream()
        .filter(s -> s.startsWith("万"))   // 过滤"万"开头
        .filter(s -> s.length() == 3)      // 过滤长度为 3
        .forEach(System.out::println);     // 遍历输出

Stream 流的好处

可以直接阅读代码的字面意思,就能展示跟逻辑无关的代码语义:

list                          ---> 集合对象
.stream()                     ---> 获取流
.filter(s -> s.startsWith("万"))  ---> 过滤(过滤方式是找"万"开头)
.forEach(System.out::println); ---> foreach 遍历(输出)

不需要再创建一个个中间集合,一行一步操作,链式调用非常直观。

关键理解

  1. filter 是过滤(筛选)操作:传入一个判断条件(Lambda),返回符合条件的新流。
  2. forEach 是遍历操作:对流中的每个元素执行传入的动作(如打印)。
  3. 只关注"做什么":不用写循环变量、不用管中间集合的增删——这正是函数式编程的体现。
  4. Stream 流才是真正把函数式编程引入到 Java 中

4. Stream 流的思想:三类方法(获取 / 中间 / 终结)

概念

Stream 流:你可以把它理解为一条流水线、水流——数据从一头进,经过中间一道道工序(过滤、去重、跳过……),最后从另一头产出结果。

// 来源:course/Demo01.java(注释部分)
/*
    Stream流的思想:
        Stream流总体来说,分为三类方法:
            1、获取方法 -- 获取Stream流
                |- 创建一条流水线,并且把数据放在流水线上准备进行操作。
            2、中间方法 -- 处理Stream流
                |- 流水线上的操作,一次操作完成后,还可以继续进行其他操作
            3、终结方法 -- 执行最后的操作
                |- 一个Stream流水线只能有一个终结方法,是流水线上的最后一个操作。
*/

三类方法对比

类别 作用 特点
获取方法 创建一条流水线,把数据放上去准备操作 一切的起点
中间方法 流水线上的操作(过滤、去重、跳过、截断、映射…) 执行完还可以继续链式调用,如 .filter().filter()
终结方法 流水线上最后一个操作(遍历、计数、收集、最值…) 一条流水线只能有一个终结方法,调用后流结束

关键理解

  1. 中间方法可以连续调用多个:如 stream.filter(...).filter(...)——每调用一次仍在"流水线上"。
  2. 终结方法只能有一个:它宣告流水线的终点,调用完这条流就结束了。
  3. 常用中间方法(filter/limit/skip/map/concat/distinct)详见第 5 节;方法引用详见第 6 节;终结方法 forEach/count 详见第 7 节;收集方法 collect + Collectors 详见第 8 节

5. Stream 流中间操作方法:filter / limit / skip / map / concat / distinct

概念

中间操作方法的意思是:执行完此方法后,Stream 流依然可以继续执行其他操作——中间方法往往会返回一个新的流对象Stream 对象),就是为了让我们可以继续进行下一步操作(链式调用)。

// 来源:course/Demo01.java(注释部分:中间操作方法定义)
/*
    Stream流中间操作方法:
        中间操作的意思是执行完此方法后,Stream流依然可以继续执行其他操作。
        常用的方法:
            Stream<T> filter(Predicate<? super T> predicate)  用于对流的数据进行过滤
            Stream<T> limit(long maxSize)   返回这个流中元素组成的流,截取前指定参数个数的数据。
            Stream<T> skip(long n)  跳过指定参数个数的数据,返回由该流剩余元素组成的流
            public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)    合并a和b两个流为一个流
            Stream<T> distinct()    返回由该流的不同元素组成的流,根据Object.equals(obj)得到是否相同。
*/

常用中间方法速查

方法 作用 说明
Stream<T> filter(Predicate<? super T> predicate) 过滤 传入断言条件,返回满足条件的元素组成的新流
Stream<T> limit(long maxSize) 截取 返回由该流中maxSize元素组成的流
Stream<T> skip(long n) 跳过 跳过前 n 个元素,返回由该流剩余元素组成的流
Stream<R> map(Function<? super T, ? extends R> mapper) 映射 把流中每个元素映射/转换成另一种类型,返回新类型组成的流
Stream<T> concat(Stream a, Stream b) 合并 ab 两个流合并为一个流(静态方法)
Stream<T> distinct() 去重 返回由该流不同元素组成的流,根据 Object.equals(obj) 判断是否相同

filter 的参数类型 Predicate<? super T> 是 JDK 内置函数式接口java.util.function.Predicate,有参有返回值、返回 boolean 的断言),昨天 0803 笔记中已介绍过。

代码示例(Demo01 的 main 方法)

// 来源:course/Demo01.java(main 方法:中间方法演示)
import java.util.function.Predicate;   // 新增 import:Predicate 函数式接口
import java.util.stream.Stream;

// 承接上文生成的流(数据已换为新示例):
Stream<String> arrayStream = Arrays.stream(new String[]{"郑伊健", "陈浩南", "齐夏", "谢文东"});
Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");

// ① filter / limit / skip 链式调用(Predicate 匿名内部类 vs Lambda 对比在注释中演示,此处先注释,思路已明确)
// Stream<String> newStream = stringStream.filter(s -> s.startsWith("万"));
// Stream<String> newNewStream = newStream.limit(2);
// newNewStream.skip(1).forEach(System.out::println);

// ② concat:合并 a 和 b 两个流为一个流(Stream 接口的静态方法)
Stream<String> concat = Stream.concat(arrayStream, stringStream);
concat.forEach(System.out::println);

// ③ distinct:去重(依据 Object.equals),重复元素只保留一个
List<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
        "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");
stringList.stream().distinct().forEach(System.out::println);

map 映射示例(Demo02):把学生流映射成姓名流

map(映射)是流中非常常用的中间方法:把流中的每个元素转换成另一种类型/形式,返回新类型的流。典型场景——学生集合中提取所有学生的姓名生成一个新集合。

// 来源:course/Demo02.java(main 方法)
// 需求:把学生集合中每个学生的 name 提取出来生成一个新的集合
// 传统写法:for 循环 + 中间集合
ArrayList<String> nameList = new ArrayList<>();
for (Student s : list) {
    nameList.add(s.getName());
}

// Stream 写法:map 把"学生"映射成"姓名",再收集成 List
List<String> nameList2 = list            // 集合对象
        .stream()                        // 获取流
        .map(student -> student.getName())  // 映射:获取每个学生对象的姓名
        .collect(Collectors.toList());   // 收集成新集合

map 的参数是 JDK 内置函数式接口 Function<T, R>(有参有返回值)——student -> student.getName() 就是 Function<Student, String> 的简化写法。

链式调用示意(流水线视角)

Stream.of("万震山", "万梓良", "万事如意", "王老五")
    .filter(姓"万")      // 中间方法 → 剩 "万震山"、"万梓良"、"万事如意"
    .limit(2)            // 中间方法 → 截取前 2 个 → "万震山"、"万梓良"
    .skip(1)             // 中间方法 → 跳过前 1 个 → "万梓良"
    .forEach(...)        // 终结方法 → 只输出 "万梓良"

关键理解

  1. 中间方法返回新流、可无限链式:每调用一个中间方法都返回新的 Stream 对象,所以可以 .filter().limit().skip() 一直写下去。
  2. filter 的 Lambda 本质s -> s.startsWith("万") 就是 Predicate<String> 匿名内部类 test() 方法的简化写法(s 是元素,返回 boolean)。
  3. limitskip 常配合分页limit(n) 截取前 n 个;skip(m) 跳过前 m 个——两者组合可实现"跳过 m 条、取 n 条"的分页效果。
  4. concat 是静态方法:它属于 Stream 接口的静态方法(不是流对象方法),用法是 Stream.concat(流a, 流b),把两个流合并成一个。
  5. distinct 依赖 equals:去重依据是 Object.equals(obj)——自定义对象要正确去重,必须重写 equals/hashCode(正好呼应前几天的 Object 类知识点)。
  6. map 是"转换"而不是"筛选"filter 是"留下符合条件的",map 是"把每个元素变成另一种东西"(如把学生变成姓名、把 Integer 变成 String)。

6. 方法引用(Method Reference)—— 四种形式

概念

方法引用Lambda 表达式的进一步简化:当 Lambda 的方法体只有一行代码,且这行代码只是调用某个已有的方法时,就可以直接用 ::(双冒号)引用那个方法,把参数按顺序传入。

// 来源:course/Demo02.java(注释部分:方法引用四种形式)
/*
    方法引用:
        引用类的静态方法:       类名::静态方法名      String::valueOf
        引用对象的实例方法:     对象名::方法名         sb::append
        引用类的实例方法:       类名::方法名           String::substring
        引用类的构造方法:       类名::new              String::new

        使用前提(四种通用):在重写方法的时候,方法体里面【只有一行代码】,并且这行代码是
        调用【某个类的静态方法 / 某个对象的成员方法 / 某个类的成员方法 / 某个类的构造方法】,
        并且我们要把重写的【抽象方法中的所有参数都按照顺序传入】到这个方法中。
*/

四种形式速查

形式 写法 示例 说明
引用类的静态方法 类名::静态方法名 String::valueOf 调用某个类的静态方法
引用对象的实例方法 对象名::方法名 sb::append 调用某个对象的成员方法(非 static)
引用类的实例方法 类名::方法名 String::substring 调用某个类的成员方法
引用类的构造方法 类名::new String::new 调用某个类的构造方法

四种形式的共同前提:方法体只有一行代码,且抽象方法的所有参数按顺序传入被引用的方法中。

代码示例一:引用类的静态方法(String::valueOf)

// 来源:course/Demo02.java(main 方法:map 后把 Integer 转成 String)
// 先 map 出每个学生的 id(Integer),再 map 用 String::valueOf 把整数转成字符串
List<String> stringIdList = list.stream()
        .map(s -> s.getId())            // 映射出学号(Integer 流)
        .map(String::valueOf)           // 静态方法引用:Integer → String(相当于 new String(valueOf(元素)))
        .collect(Collectors.toList());
System.out.println(stringIdList);
// 输出:[1001, 1002, 1003, ...]

代码示例二:引用对象的实例方法(sb::append)

// 来源:course/Demo02.java(main 方法:把每个学生姓名追加到 StringBuilder)
StringBuilder sb = new StringBuilder();
list.stream()
        .map(student -> student.getName())   // 取出姓名流
        // 简化前——匿名内部类 Consumer
        // .forEach(new Consumer<String>() {
        //     public void accept(String s) { sb.append(s); }
        // })
        // 简化后——Lambda 表达式
        // .forEach(s -> sb.append(s));
        .forEach(sb::append);                // 对象方法引用:对每个元素调用 sb.append(元素)
System.out.println(sb);

代码示例三:引用类的实例方法(String::substring —— MethodDemo 四种实现方式对比)

// 来源:course/MethodDemo.java
// 自定义函数式接口(内部接口):截取字符串
public interface UseString {
    String use(String str, int start, int length);
}

public static String subName(String name, UseString us) {
    return us.use(name, 0, 1);   // 截取第 0 位开始的 1 个字符(取姓氏)
}

public static void main(String[] args) {
    // 方式 1:传统实现类
    UseStringImpl usi = new UseStringImpl();
    String lastName = subName("张三", usi);              // 张

    // 方式 2:匿名内部类
    String lastName02 = subName("李四", new UseString() {
        public String use(String str, int start, int length) {
            return str.substring(start, length);
        }
    });

    // 方式 3:Lambda 表达式
    String lastName03 = subName("李四", (str, start, length) -> str.substring(start, length));

    // 方式 4:引用类的实例方法 —— 类名::方法名(str.substring(start,length) 完全匹配)
    String lastName04 = subName("王五", String::substring);
}

方式 4 为什么能成立:重写的抽象方法 use(String str, int start, int length) 方法体只有一行 str.substring(start, length)——它调用的是某个类(String)的成员方法,且抽象方法的三个参数 str, start, length 按顺序正好对应 substring 的参数,所以可以直接写成 String::substring

代码示例四:引用类的构造方法(String::new)

// 来源:course/Demo02.java(main 方法:把姓名创建成 String 对象)
list.stream()
        .map(name -> name.getName())   // 取出姓名流
        .map(String::new)              // 构造方法引用:等价于 new String(name)
        .forEach(System.out::println);

关键理解

  1. 方法引用是 Lambda 的语法糖String::valueOfs -> String.valueOf(s)sb::appends -> sb.append(s)String::news -> new String(s)
  2. 本质仍是函数式接口:方法引用替换的仍是 Lambda,底层还是函数式接口的实现——String::substring 实现的是 UseString 接口的 use() 方法。
  3. 使用前提是"一行代码 + 参数顺序对应":四种形式都必须满足"方法体只有一行代码、抽象方法的所有参数按顺序传入",否则不能使用方法引用。
  4. 区分三种实例方法引用对象名::方法名(如 sb::append,针对某个具体对象)与 类名::方法名(如 String::substring,第一个参数隐式作为调用对象)要分清——类名::方法名 形式下,抽象方法的第一个参数就是调用成员方法的那个对象。
  5. 与 0803 已学方法的衔接Comparator.comparing(Employee::getName) 就是"引用类的实例方法"的一种应用(getName 是 Employee 的成员方法)。

7. Stream 流终结操作方法:forEach / count

概念

终结操作方法的意思:执行完这个方法后,Stream 流就不能再执行其他操作了——一条流水线只能有一个终结方法,它是流水线上的最后一个操作。

// 来源:course/Demo01.java(注释部分:终结操作方法定义)
/*
    Stream流终结操作方法:
        终结操作的意思就是执行完这个方法后,Stream流就不能再执行其他操作了。
        void forEach(Consumer<? super T> action);   对此流的每个元素执行操作(不一定是输出)
        long count();   返回此流中的元素数
*/

常用终结方法速查

方法 作用 说明
void forEach(Consumer<? super T> action) 遍历 对流的每个元素执行操作(不一定是输出,也可以做其他事)
long count() 计数 返回此流中的元素个数(注意返回值是 long

forEach 的参数 Consumer<? super T> 是 JDK 内置函数式接口java.util.function.Consumer,有参无返回值)——昨天 0803 笔记中介绍过。

关键理解

  1. 终结方法 = 流水线终点:调用 forEach/count 后,这条流就结束了,不能再继续 .filter() 等操作。
  2. forEach 不一定只是输出:它的 Consumer 参数可以执行任意动作(打印、计数、写入集合等),System.out::println 只是最常见的一种(方法引用)。
  3. count 返回 long:元素个数用 long 表示,配合中间方法可统计"过滤后还剩多少个"。

8. Stream 流收集操作方法:collect + Collectors(toList / toSet / toMap)

概念

收集操作方法:当我们使用流对一些数据操作完毕后,可以把流中的数据收集到集合(List / Set / Map / 数组)中——collect 也是终结操作的一种,但它把结果"存起来"而非直接输出。

// 来源:course/Demo01.java(注释部分:收集操作方法定义)
/*
    Stream流收集操作方法:
        当我们使用流对一些数据操作完毕后,可以把流中的数据收集到集合中。
        <R, A> R collect(Collector<? super T, A, R> collector);     把结果收集到集合中
        Object[] toArray(); 把结果收集到数组,生成一个新的结果集数组
        default List<T> toList()    把结果收集到List集合,生成一个新的List集合,这里返回的List集合是不可变的集合。
    工具类Collectors提供了具体的收集方式:
        toMap(Function keyMapper, Function valueMapper)    把元素收集到map集合
        toList()    把元素收集到List集合
        toSet()     把元素收集到Set集合
*/

收集方式速查

方法 收集目标 说明
collect(Collector collector) 集合 配合 Collectors 工具类的具体收集器使用
toArray() 数组 把结果收集成新数组
toList() List 把结果收集成新 List——返回的 List 是不可变集合(不能增删改)
Collectors.toList() List 工具类 Collectors 提供的收集方式
Collectors.toSet() Set 工具类 Collectors 提供的收集方式(Set 天然去重)
Collectors.toMap(keyMapper, valueMapper) Map 工具类 Collectors 提供的收集方式,两个参数都是 Function 函数式接口

代码示例一:Demo01 —— collect 收集成 List / Set

// 来源:course/Demo01.java(main 方法:收集操作演示)
import java.util.stream.Collectors;   // 新增 import:Collectors 收集工具类

List<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
        "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");

// ① 去重后收集成 List 集合
List<String> list1 = stringList.stream()   // 集合对象
        .distinct()                        // 去重
        .collect(Collectors.toList());     // 收集成 List
System.out.println(list1);

// ② 过滤"伊"后收集成 Set 集合(Set 天然去重,即使不过滤 distinct 也只有不重复的)
Set<String> set = stringList.stream()            // 集合对象
        .filter(s -> s.contains("伊"))           // 过滤包含"伊"的
        .collect(Collectors.toSet());            // 收集成 Set
System.out.println(set);

代码示例二:ToMapDemo —— collect 收集成 Map(重点)

新增两个文件:Student 实体类 + ToMapDemo 演示类。

// 来源:course/Student.java(实体类,作为 toMap 的值对象)
public class Student {
    private Integer id;      // 学号
    private String name;     // 姓名
    // 无参构造 / 有参构造(id, name)
    // getter / setter
    // equals / hashCode:基于 id + name(Objects.equals / Objects.hash)
    // toString()
}
// 来源:course/ToMapDemo.java(main 方法)
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

// 创建 7 名学生放入 ArrayList
Student s1 = new Student(1001, "项羽");
Student s2 = new Student(1002, "张良");
// ... s3~s7 略(项庄/刘邦/萧何/韩信/樊哙)
ArrayList<Student> list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7));

/*
    Collectors.toMap 的完整签名:
    public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
        Function<? super T, ? extends K> keyMapper,   // 取"键"的函数
        Function<? super T, ? extends U> valueMapper  // 取"值"的函数
    )
*/

// 方式一:Lambda 写法 —— toMap(键, 值)
Map<Integer, Student> map = list                 // 集合对象
        .stream()                                // 生成流
        .collect(Collectors.toMap(
                student -> student.getId(),      // 键:学号 id
                student -> student               // 值:学生对象本身
        ));
System.out.println(map);

// 方式二:匿名内部类写法(Function 函数式接口,与 Lambda 等价)
Map<Integer, Student> map1 = list.stream()
        .collect(Collectors.toMap(
                new Function<Student, Integer>() {            // keyMapper
                    @Override
                    public Integer apply(Student student) {
                        return student.getId();
                    }
                },
                new Function<Student, Student>() {            // valueMapper
                    @Override
                    public Student apply(Student student) {
                        return student;
                    }
                }
        ));

关键理解

  1. collect 是终结操作:调用完流就结束,但结果不是直接输出,而是收集到集合/数组,方便后续继续使用。
  2. Collectors 是收集工具类:类比 Arrays(数组工具类)、Collections(集合工具类),Collectors 专门为流提供收集器——toList() / toSet() / toMap()
  3. toMap 的两个参数都是 FunctionFunction<T, R> 是 JDK 内置函数式接口(有参有返回值)——第一个函数从元素中取(keyMapper),第二个函数从元素中取(valueMapper)。student -> student.getId() 取学号当键,student -> student 取学生对象当值。
  4. Lambda vs 匿名内部类student -> student.getId() 就是 new Function<Student, Integer>(){...} 的简化写法,两种方式等价,Lambda 更简洁(对应 0803 笔记的省略规则)。
  5. toList()Stream.toList() 返回不可变集合:收集出的 List 不能 add/remove(呼应 List.of() 不可变集合的坑)。
  6. toSet() 天然去重:收集成 Set 本身就保证元素不重复。
  7. toMap 键冲突会抛异常:如果多个元素映射出相同的键,Collectors.toMap 默认抛 IllegalStateException——所以键必须是唯一的(如学号 id)。
  8. 贪心思想注释:ToMapDemo 注释中提到的"贪心思想——局部最优解,不代表全局最优解",是程序设计中的一个思维比喻,提醒我们局部最优不等于全局最优。

9. 生成 Stream 流的四种方式

数据来源对应四种生成方式

数据来源 生成方式 说明
Collection 体系集合 集合对象.stream() 使用默认方法 default Stream<E> stream()
Map 体系集合 map.keySet().stream() / map.values().stream() 把 Map 转换成 Set 集合,间接生成流
数组 Arrays.stream(数组) Arrays 静态方法 public static IntStream stream(int[] array)
相同类型的多个数据 Stream.of(T... value) Stream 接口的静态方法 of(T...value)

代码示例(Demo01 的 main 方法)

// 来源:course/Demo01.java
package course;

import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Demo01 {
    public static void main(String[] args) {
        // 1. Collection体系集合:使用默认方法 stream() 生成流
        List<String> list = new ArrayList<>();
        Stream<String> stream = list.stream();

        // 2. Map体系集合:把 Map 转换成 Set,间接的生成流
        Map<String, Object> map = new HashMap<>();
        Stream<String> keyStream = map.keySet().stream();   // key 就是 Set 集合
        Stream<Object> valueStream = map.values().stream(); // value 就是 List 集合

        // 3. 数组:通过 Arrays 中的静态方法 stream 生成流
        String[] array = {"郑伊健", "陈浩南", "齐夏", "谢文东"};
        Stream<String> arrayStream = Arrays.stream(array);

        // 4. 相同类型的多个数据:通过 Stream 接口的静态方法 of(T...value) 生成流
        Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
        Stream<Integer> integerStream = Stream.of(10, 15, 20, 35, 46);
    }
}

关键理解

  1. stream() 是 Collection 接口的默认方法List / Set / Queue 这些 Collection 体系集合都能直接调用。
  2. Map 不能直接生成流:因为 Map 不是 Collection 体系,需要先把 keySet()(Set)或 values()(Collection)取出来再 .stream()
  3. Arrays.stream() 专门处理数组:数组不是集合,不能调 .stream(),要用 Arrays 工具类的静态方法。
  4. Stream.of(T... values) 处理零散数据:注意 of 的参数是可变参数T...)——正好用上了昨天学的 Varargs!Stream.of(10, 15, 20, 35, 46) 本质就是 Stream.of(new Integer[]{10,15,20,35,46})

10. 20260803 课后作业回顾:Lambda 表达式应用(Addable / Flyable)

作业场景回顾

调用某个方法的时候,需要做哪些事情?核心思路:

  1. 先看所属的类
  2. 再看修饰符(访问权限修饰符、静态修饰符)
    • 如果是 private 修饰符 → 肯定调用不了
    • 如果是 static 修饰 → 类名.方法名() 直接调用;否则创建对象用对象调用
  3. 看方法的参数:只要不是那 8 个基本数据类型,其他的都是引用数据类型变量,都需要传入对象

    • 传入的对象必须是该参数类型的,或者是该参数类型的子类、实现类的对象——这就形成了多态

      // 来源:homework0803/p4_lambda/LambdaDemo.java(注释部分)
      // 只要不是那8个基本数据类型,其他的都是引用数据类型变量。都需要在调用方法的时候传入 【对象】
      // 传入的对象必须是该参数类型的,或者是该参数类型的子类、实现类的对象。 这就形成了多态
      

函数式接口定义

// 来源:homework0803/p4_lambda/Addable.java
// 有参有返回值的函数式接口
package homework0803.p4_lambda;

public interface Addable {
    int add(int x, int y);
}
// 来源:homework0803/p4_lambda/Flyable.java
// 有参无返回值的函数式接口
package homework0803.p4_lambda;

public interface Flyable {
    void fly(String s);
}

Lambda 传参调用

// 来源:homework0803/p4_lambda/LambdaDemo.java
package homework0803.p4_lambda;

public class LambdaDemo {

    public static void useAdd(Addable a) {
        System.out.println(a.add(10, 20));
    }

    public static void useFlyable(Flyable f) {
        f.fly("今天天气不错...是风和日丽的...");
    }

    public static void main(String[] args) {
        // 调用 useAdd 方法,传入参数有两种选择:
        //   1、自己新建一个实现类,实现接口,重写方法。
        //   2、创建该接口的匿名内部类对象。
        // 现在还有第 3 种:直接传入 Lambda 表达式(本质是简化后的匿名内部类)
        useAdd((x, y) -> x + y);   // 有参有返回值:单语句 → 省略大括号、分号、return
    }
}

知识点总结

知识点 说明
调用方法三步看 ①所属类 ②修饰符(private 不能调 / static 类名调 / 否则对象调)③参数类型
引用类型参数传对象 参数不是 8 大基本类型时,就要传该类型或其子类/实现类的对象(多态)
Lambda 是第三种传参方式 方法参数是函数式接口时,可传实现类对象、匿名内部类对象,或直接传 Lambda
useAdd((x, y) -> x + y) 两个参数(小括号不能省),方法体只有一条表达式语句(省大括号/分号/return)

11. 20260803 课后作业回顾:Employee 员工管理(TreeSet 自然排序 + Comparator 按姓名 + 方法引用)

作业要求回顾

创建 TreeSet<Employee>(自然排序),添加 5 名员工(其中两名工号相同,测试去重):按薪资降序、薪资相同再按工号;再用迭代器遍历输出。最后通讯录按姓名排序:重新创建 TreeSet 传入按姓名排序的 Comparator。

Employee 实体类:实现 Comparable + equals/hashCode

// 来源:homework0803/p5_employee/Employee.java
package homework0803.p5_employee;

import java.util.Objects;

public class Employee implements Comparable<Employee> {
    private String id;      // 工号
    private String name;    // 姓名
    private double salary;  // 薪资
    private String dept;    // 部门

    public Employee(String id, String name, double salary, String dept) {
        this.id = id;
        this.name = name;
        this.salary = salary;
        this.dept = dept;
    }

    // getter / setter ...(略)

    // 自然排序:按薪资降序,薪资相同再按工号
    @Override
    public int compareTo(Employee o) {
        int result = Double.compare(o.getSalary(), this.salary);  // 降序:交换参数
        return result == 0 ? this.id.compareTo(o.getId()) : result;  // 薪资相同按工号
    }

    // equals / hashCode:以工号判定(与 compareTo 的"工号"判定口径一致)
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Employee e) {
            return Objects.equals(id, e.id);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

EmployeeSystem:TreeSet 自然排序 + 迭代器遍历

// 来源:homework0803/p5_employee/EmployeeSystem.java(部分)
package homework0803.p5_employee;

import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

public class EmployeeSystem {
    public static void main(String[] args) {
        // 1. 创建 TreeSet<Employee>(自然排序:按薪资降序,薪资相同按工号;工号重复自动去重)
        TreeSet<Employee> treeSet = new TreeSet<>();
        treeSet.add(new Employee("E001", "张三", 12000, "技术部"));
        treeSet.add(new Employee("E002", "李四", 15000, "市场部"));
        treeSet.add(new Employee("E003", "王五", 15000, "技术部"));
        treeSet.add(new Employee("E004", "赵六", 9000,  "财务部"));
        treeSet.add(new Employee("E005", "刘七", 12000, "市场部"));
        treeSet.add(new Employee("E002", "李四", 20000, "市场部"));  // 工号重复 → 被去重

        // 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重
        Iterator<Employee> iterator = treeSet.iterator();
        while (iterator.hasNext()) {
            Employee e = iterator.next();
            System.out.println(e.getId() + "-" + e.getName() + "-" + e.getSalary() + "-" + e.getDept());
        }

        // 3. 通讯录按姓名排序:重新创建 TreeSet,传入按姓名排序的 Comparator(此处用方法引用)
        treeSet = new TreeSet<>(Comparator.comparing(Employee::getName));
        // 再添加 6 名员工(同上),迭代器遍历输出 → 按姓名排序
    }
}

方法引用(Method Reference)—— 新知识

方法引用的四种形式(静态方法 / 对象实例方法 / 类实例方法 / 构造方法)详见第 6 节,本节是它在 Comparator 中的一次具体应用。

Comparator.comparing(Employee::getName) 里的 Employee::getName 就是方法引用——它是 Lambda 的一种更简化的写法

写法 代码 说明
匿名内部类 new Comparator<Employee>() { compare(...) { return o1.getName().compareTo(o2.getName()); } } 最原始,最啰嗦
Lambda 表达式 (o1, o2) -> o1.getName().compareTo(o2.getName()) 省略类型、大括号、return
方法引用 Comparator.comparing(Employee::getName) 类名::方法名 直接引用现有方法,最简洁
// 来源:homework0803/p5_employee/EmployeeSystem.java(注释提示)
// 可用匿名内部类,也可尝试用 Lambda:Comparator.comparing(Employee::getName) 或
// (o1, o2) -> o1.getName().compareTo(o2.getName())

知识点总结

知识点 说明
自然排序 + 二次比较 compareTo 先按薪资(Double.compare),薪资相同返回 0 时再按工号比较,保证唯一
降序技巧 交换 compare 参数位置:Double.compare(o.getSalary(), this.salary) = 薪资降序
compareTo / equals / hashCode 一致性 都以工号判定口径 → TreeSet 去重(E002 重复被丢弃)与排序行为一致,符合官方推荐
instanceof 模式匹配 if (obj instanceof Employee e) 直接得到强转后的变量 e(JDK16+ 特性)
Comparator.comparing(类::getter) 方法引用写法,TreeSet<>(Comparator.comparing(Employee::getName)) = 按姓名排序
方法引用 vs Lambda 方法引用是 Lambda 的进一步简化:Employee::getName(e) -> e.getName()

12. 随堂练习要点

练习一:Stream 流筛选集合元素

核心步骤

  1. 创建 ArrayList<String>,存入"万震山、万梓良、万事如意、王老五、万巨龙、张三"
  2. list.stream().filter(s -> s.startsWith("万")).forEach(...) 筛选并遍历"万"开头
  3. 再用 .filter(s -> s.length() == 3) 叠加筛选长度为 3 的元素

思考

  1. 传统 for 循环要写几个集合?Stream 流为什么不用中间集合?—— 传统写法每筛选一次要建一个新集合;Stream 用 filter 链式过滤,数据在流水线上流动,不需要中间容器
  2. .filter() 属于三类方法中的哪一类?—— 中间方法(可连续调用多个);.forEach() 呢?—— 终结方法(只能有一个,流水线终点)

练习二:四种方式生成 Stream 流

核心步骤

  1. Collection 集合:list.stream()
  2. Map 集合:map.keySet().stream() / map.values().stream()
  3. 数组:Arrays.stream(array)
  4. 零散数据:Stream.of(10, 15, 20, 35, 46)

思考

  1. Map 为什么不能直接 .stream()?—— Map 不是 Collection 体系,要先 keySet()values() 取出 Set/Collection
  2. Stream.of(T... values) 用了昨天学的什么语法?—— 可变参数 Varargs(T...),所以可以传任意个数数据

练习三:函数式接口 + Lambda 传参(作业一)

核心步骤

  1. 定义有参有返回值的函数式接口 Addableint add(int x, int y)
  2. 定义有参无返回值的函数式接口 Flyablevoid fly(String s)
  3. 定义 useAdd(Addable a) / useFlyable(Flyable f) 方法,调用时直接传 Lambda
  4. useAdd((x, y) -> x + y) 体会单语句省略大括号/分号/return

思考

  1. 方法参数是函数式接口时,能传哪几种东西?—— 实现类对象、匿名内部类对象、Lambda 表达式(三种都能体现多态)
  2. 调用方法前"三步看"是什么?—— 看所属类 → 看修饰符(private/static)→ 看参数类型(引用类型要传对象)

练习四:Employee 员工排序(作业二)

核心步骤

  1. Employee 实现 Comparable<Employee>compareTo 按薪资降序、薪资相同按工号
  2. equals / hashCode 以工号判定(与 compareTo 口径一致)
  3. TreeSet<Employee> 存入 6 名员工(含工号重复的 E002),迭代器遍历观察排序 + 去重
  4. 重新 new TreeSet<>(Comparator.comparing(Employee::getName)) 按姓名排序,迭代器遍历

思考

  1. E001(12000)和 E005(12000)薪资相同为什么都能存入?—— compareTo 薪资相同后继续比较工号(E001 ≠ E005),返回值不为 0,不视为重复
  2. 工号重复的 E002 为什么被去重?—— compareTo 判定重复(0)且 equals/hashCode 也按工号判定,TreeSet 去重规则生效
  3. Comparator.comparing(Employee::getName) 中的 Employee::getName 是什么写法?—— 方法引用,等价于 Lambda (e) -> e.getName()
  4. 方法引用比 Lambda 省了什么?—— 省掉参数列表和箭头,直接用 类名::方法名 引用现有方法(当前作业已用上)

练习五:Stream 流中间操作方法(filter / limit / skip / concat / distinct)

核心步骤

  1. Stream.of("万震山", "万梓良", "万事如意", "王老五") 生成流
  2. .filter(s -> s.startsWith("万")) 过滤出"万"开头 → 再 .limit(2) 截取前 2 个 → 再 .skip(1) 跳过前 1 个 → .forEach(System.out::println) 输出
  3. 体会"每个中间方法都返回新的流对象",所以能一直链式调用
  4. 尝试 Stream.concat(流a, 流b) 合并两个流;给包含重复元素的数据加 .distinct() 去重

思考

  1. 中间方法和终结方法怎么区分?—— 中间方法执行完返回新的 Stream,还能继续链式操作;终结方法执行完流就结束,不能再调用其他方法
  2. filter 的 Lambda s -> s.startsWith("万") 本质是什么?—— 是 Predicate<String> 函数式接口 test() 方法的简化(有参、返回 boolean 的断言)
  3. limitskip 组合能实现什么?—— 分页:skip(m) 跳过前 m 条、limit(n) 取 n 条
  4. distinct 去重的依据是什么?—— Object.equals(obj),自定义对象要重写 equals/hashCode 才能正确去重

练习六:Stream 流终结操作方法(forEach / count)

核心步骤

  1. list.stream().forEach(System.out::println) 遍历输出每个元素
  2. long count = list.stream().filter(s -> s.contains("伊")).count(); 统计过滤后元素个数
  3. 尝试在 forEach 之后继续调用 .filter(...),观察编译/运行结果

思考

  1. 为什么 forEach 之后不能再调用其他方法?—— forEach 是终结操作,调用后流就结束了,不能继续链式操作
  2. forEach 的参数 Consumer 一定只能打印吗?—— 不一定,Consumer<T> 是有参无返回值的函数式接口,可以做任意动作(打印、计数、写入等)
  3. count() 的返回值类型是什么?—— long,不是 int

练习七:Stream 流收集操作(collect / Collectors:toList / toSet / toMap)

核心步骤

  1. list.stream().distinct().collect(Collectors.toList()) 去重后收集成 List,打印观察
  2. list.stream().filter(...).collect(Collectors.toSet()) 过滤后收集成 Set,观察 Set 天然去重
  3. 定义 Student 实体类(id、name、getter/setter、equals/hashCode、toString),创建 7 名学生
  4. list.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)) 收集成 Map<Integer, Student>,键是学号、值是学生对象
  5. 尝试把 s -> s.getId() 改写成匿名内部类 new Function<Student, Integer>() {...},对比两种写法

思考

  1. collect 是中间操作还是终结操作?—— 终结操作(执行完流结束),但结果不是直接输出,而是收集到集合/数组
  2. Collectors.toMap 的两个参数分别是什么?—— 第一个 keyMapper 从元素中取,第二个 valueMapper 从元素中取,都是 Function 函数式接口
  3. 为什么用学号当键?—— 键必须唯一;如果两个元素映射出相同键,toMap 默认抛 IllegalStateException
  4. collect(Collectors.toList()) 得到的 List 能 add 吗?—— 不能,Collectors.toList() / Stream.toList() 返回的是不可变集合,直接增删改抛 UnsupportedOperationException
  5. Lambda s -> s.getId() 与匿名内部类 new Function<Student, Integer>(){...} 是什么关系?—— 等价,Lambda 是后者的简化写法

随堂练习参考解答(已提供代码)

对应今日随堂练习的参考解答已写入授课代码:

练习 参考代码路径
练习 3 分页:limit/skip 分页 + Stream.toList() exericse/exericse03/PageTest.java

PageTest 参考解答要点

// 来源:exericse/exericse03/PageTest.java
// 1. limit(5):截取前 5 个
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).limit(5).forEach(i -> System.out.print(i + " "));

// 2. skip(3):跳过前 3 个
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).skip(3).forEach(i -> System.out.print(i + " "));

// 3. 用 Stream.toList() 把流收集成 List(注意:返回不可变集合)
List<Integer> list = Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).toList();

// 分页方法:skip((pageNo-1) * pageSize).limit(pageSize)
public static void showPage(List<Integer> list, int pageNo, int pageSize) {
    list.stream()
        .skip((pageNo - 1) * pageSize)   // 跳过前面的页
        .limit(pageSize)                 // 截取本页数量
        .forEach(i -> System.out.print(i + " "));
}
showPage(list, 1, 3);   // 0 1 2
showPage(list, 2, 3);   // 3 4 5
showPage(list, 3, 3);   // 6 7 8
showPage(list, 4, 3);   // 9

13. 拓展阅读

官方文档

推荐阅读

  • 《Java 8 实战》第 4 章 引入流、第 5 章 使用流 —— Stream 流的入门与常用操作
  • 《Java 8 实战》第 3 章 Lambda 表达式 —— 方法引用、复合 Lambda
  • 《Effective Java》第 3 版 第 42~44 条:Lambda 优先于匿名类、方法引用优先于 Lambda
  • 《Java 核心技术 卷 I》第 6 章 接口、lambda 表达式与内部类
  • 《Java 8 实战》第 3 章 3.3 节 —— 方法引用:四种形式与使用场景

相关知识点

前置知识 当前知识 后续知识
匿名内部类(0727) 函数式编程思想 方法引用、Stream 高阶函数
Lambda 表达式(0803) Lambda 应用(Addable/Flyable) Stream API 的 filter/map/collect
Comparable/Comparator(0803) TreeSet 员工排序 + 方法引用 Stream 流的 sorted 排序、Collectors
可变参数 Varargs(0803) Stream.of(T... values) 可变参数应用 方法引用 & 收集器
Collection 体系集合(0729) 生成 Stream 流的四种方式 中间方法(filter/limit/skip/distinct/map)
函数式接口 Predicate(0803) 中间操作方法(filter/limit/skip/concat/distinct) Stream 映射 map、终结方法 collect/count、Collectors 收集器
函数式接口 Consumer/Function(0803) 终结操作 forEach/count、收集操作 collect/Collectors(toList/toSet/toMap) Collectors 高级用法(groupingBy 分组、joining 拼接)、Stream 与集合互转
Lambda 表达式(0803) 方法引用四种形式(静态/对象实例/类实例/构造) 方法引用的高级组合、Stream 中方法引用大量应用