20260804-Stream流练习.md 25 KB

2026-08-04 Stream 流与函数式编程专项练习(课堂练习用)

说明:以下习题围绕今日授课核心内容——函数式编程与 Stream 流 展开,涵盖 Stream 流的四种生成方式(Collection.stream() / Map 转 Set / Arrays.stream() / Stream.of())、中间操作方法(filter / limit / skip / concat / distinct)、终结操作方法(forEach / count)、收集操作方法(collect + Collectors.toList / toSet / toMap)、Stream 链式调用与分页思想,以及 Stream 中 Lambda 表达式(Predicate / Consumer / Function 函数式接口)与方法引用的应用等知识点。

题目分为 基础练习(生成流 / 中间操作 / 终结操作 / 收集操作,6 题)和 进阶挑战(Collectors.toMap 收集成 Map / 综合实战,2 题)。


第一部分:基础练习

以下练习围绕今日授课核心内容展开,由浅入深。


练习 1:生成 Stream 流的四种方式

难度:⭐ 知识点stream() 默认方法、Map keySet()/values()Arrays.stream()Stream.of(T...values)、可变参数

场景描述:Stream 流操作的第一步是获取流(创建一条流水线,把数据放上去)。不是所有数据都能直接调 .stream()——Collection 体系集合可以用默认方法 stream();Map 要转成 Set 间接生成;数组要用 Arrays.stream();零散数据用 Stream.of()

题目要求

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

public class StreamCreateTest {
    public static void main(String[] args) {
        // 1. Collection 体系集合:使用默认方法 stream() 生成流
        List<String> list = new ArrayList<>(List.of("张三", "李四", "王五"));
        Stream<String> stream = list.stream();   // 集合.stream()

        // 2. Map 体系集合:把 Map 转换成 Set,间接生成流
        Map<String, Object> map = new HashMap<>();
        // TODO: 用 keySet() 取键集合生成流,赋给 keyStream
        //       用 values() 取值集合生成流,赋给 valueStream

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

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

思考题

  1. Map 为什么不能直接调用 .stream()?(提示:Map 不属于 Collection 体系)
  2. Stream.of(10, 15, 20, 35, 46) 用到了之前学的什么语法?(提示:可变参数 T...
  3. Arrays.stream(array)list.stream() 有什么区别?为什么数组不能调 .stream()

练习 2:filter 过滤 + forEach 遍历(筛选集合元素)

难度:⭐⭐ 知识点filter(Predicate)forEach(Consumer)、Lambda 表达式、startsWith()/length() 字符串方法

场景描述:Stream 流中最常用的操作——过滤(filter)和遍历(forEach)。今天课上的案例:从集合中筛选出所有以"万"开头的元素,再筛选出其中长度为 3 的元素。

题目要求

import java.util.ArrayList;
import java.util.List;

public class FilterTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三"));

        // 1. 用 Stream 流筛选出所有以"万"开头的元素,并遍历输出
        //    TODO: list.stream().filter(...).forEach(System.out::println);
        //          过滤条件:s -> s.startsWith("万")

        // 2. 再叠加一次过滤:以"万"开头 且 长度为 3 的元素,遍历输出
        //    TODO: 连续调用两次 filter

        // 3. 挑战:用 count() 统计"万"开头且长度为 3 的元素个数
        //    TODO: long count = list.stream()...count();  System.out.println(count);
    }
}

预期输出

========== 以"万"开头 ==========
万震山
万梓良
万事如意
万巨龙
========== 以"万"开头 且 长度为 3 ==========
万震山
万梓良
万巨龙
========== 个数统计 ==========
3

思考题

  1. .filter() 的参数 s -> s.startsWith("万") 本质是什么?(提示:Predicate<String> 函数式接口 test() 方法的简化)
  2. 为什么可以连续写两个 .filter()?(提示:中间方法返回新的流对象,可链式调用)
  3. .forEach(System.out::println) 用到了什么写法?(提示:方法引用,等价于 s -> System.out.println(s)

练习 3:中间方法链式调用——filter / limit / skip(分页)

难度:⭐⭐ 知识点filterlimit(long maxSize)skip(long n)、链式调用、分页思想

场景描述:中间方法执行完返回新的流对象,可以一直链式调用。limit(n) 截取前 n 个元素,skip(m) 跳过前 m 个元素——两者组合可以轻松实现分页功能(跳过 m 条、取 n 条)。

题目要求

import java.util.stream.Stream;

public class PageTest {
    public static void main(String[] args) {
        // 1. 生成长度 0~9 的整数流,截取前 5 个(limit(5)),遍历输出 → 期望 0 1 2 3 4
        //    TODO: Stream.of(0,1,2,3,4,5,6,7,8,9).limit(5).forEach(...)

        // 2. 生成长度 0~9 的整数流,跳过前 3 个(skip(3)),遍历输出 → 期望 3 4 5 6 7 8 9

        // 3. 分页:集合共 10 条数据,每页 3 条
        //    第 1 页:skip(0).limit(3)  → 0 1 2
        //    第 2 页:skip(3).limit(3)  → 3 4 5
        //    第 3 页:skip(6).limit(3)  → 6 7 8
        //    第 4 页:skip(9).limit(3)  → 9
        //    TODO: 封装一个方法 showPage(List<Integer> list, int pageNo, int pageSize)
        //          公式:skip((pageNo-1) * pageSize).limit(pageSize)
    }
}

预期输出

========== limit(5) 截取前 5 个 ==========
0 1 2 3 4
========== skip(3) 跳过前 3 个 ==========
3 4 5 6 7 8 9
========== 分页:每页 3 条 ==========
第 1 页:0 1 2
第 2 页:3 4 5
第 3 页:6 7 8
第 4 页:9

思考题

  1. limitskip 都是中间方法吗?它们各自返回什么?(提示:返回新的流对象,可继续链式调用)
  2. 分页的通用公式是什么?为什么第一页要 skip(0)?(提示:skip((pageNo-1)*pageSize).limit(pageSize)
  3. 如果数据不够一页(如第 4 页只剩 1 条),limit(3) 会报错吗?(提示:不会,取到末尾就结束)

练习 4:concat 合并两流 + distinct 去重

难度:⭐⭐ 知识点Stream.concat(stream a, stream b)distinct()Object.equals、不可变集合 List.of

场景描述concat 是 Stream 接口的静态方法,把两个流合并成一个流;distinct 是去重中间方法,依据 Object.equals(obj) 判断元素是否相同。

题目要求

import java.util.List;
import java.util.stream.Stream;

public class ConcatDistinctTest {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("长贵", "赵四", "刘能");
        Stream<String> streamB = Stream.of("旺财", "长贵", "赵四");

        // 1. concat:合并两个流为一个流,并遍历输出
        //    TODO: Stream.concat(streamA, streamB).forEach(System.out::println);

        // 2. distinct:去重后再遍历输出(去掉重复的"长贵""赵四")
        //    TODO: Stream.concat(streamA, streamB).distinct().forEach(System.out::println);

        // 3. 给含重复元素的集合去重后遍历输出
        List<String> list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能", "长贵", "赵四");
        //    TODO: list.stream().distinct().forEach(System.out::println);
    }
}

预期输出

========== concat 合并(有重复) ==========
长贵 赵四 刘能 旺财 长贵 赵四
========== concat + distinct 去重 ==========
长贵 赵四 刘能 旺财
========== List 去重 ==========
长贵 赵四 郑伊健 陈浩南 刘能

思考题

  1. concat 是流对象的方法还是 Stream 接口的静态方法?怎么调用?(提示:Stream.concat(a, b),不是 a.concat(b)
  2. distinct 判断重复的依据是什么?(提示:Object.equals(obj)——自定义对象需重写 equals/hashCode)
  3. List.of(...) 返回的集合能 add 吗?(提示:不可变集合,直接修改抛 UnsupportedOperationException)

练习 5:终结操作方法——count 计数 + forEach 遍历

难度:⭐⭐ 知识点:终结方法概念、long count()forEach(Consumer)、终结方法只能有一个

场景描述:终结操作的意思是执行完这个方法后,Stream 流就不能再执行其他操作了。forEach 对每个元素执行动作,count 返回流中元素个数——它们都是流水线的最后一步

题目要求

import java.util.List;

public class TerminateTest {
    public static void main(String[] args) {
        List<String> list = List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三");

        // 1. forEach 遍历:输出每个元素
        //    TODO: list.stream().forEach(System.out::println);

        // 2. count 计数:统计以"万"开头的元素个数(filter + count)
        //    TODO: long count = list.stream().filter(s -> s.startsWith("万")).count();
        //    System.out.println(count);   // 期望 4

        // 3. 挑战:在 forEach 之后再调用 .count(),观察会发生什么?
        //    TODO: list.stream().forEach(System.out::println).count();  // 能编译吗?
    }
}

预期输出

========== forEach 遍历 ==========
万震山 万梓良 万事如意 王老五 万巨龙 张三
========== count 计数 ==========
以"万"开头的有:4 个
========== 挑战结果 ==========
forEach 之后再调 count() 会编译报错 —— 终结方法只能有一个,且调用后流已结束

思考题

  1. forEachcount 属于三类方法中的哪一类?(提示:终结方法)
  2. count() 的返回值是什么类型?(提示:long
  3. 为什么一条流水线只能有一个终结方法?(提示:终结方法执行后流就结束了)

练习 6:collect 收集操作——Collectors.toList() / toSet()

难度:⭐⭐⭐ 知识点collect(Collector)Collectors.toList()Collectors.toSet()、收集结果不可变、Set 天然去重

场景描述:使用流对数据操作完毕后,可以把流中的数据收集到集合中——collect 是终结操作,配合工具类 CollectorstoList() / toSet() 把结果存起来,方便后续继续使用。

题目要求

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class CollectTest {
    public static void main(String[] args) {
        List<String> list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能",
                "郑伊健", "陈浩南", "郑伊健", "陈浩南");

        // 1. 去重后收集成 List 集合
        //    TODO: List<String> list1 = list.stream().distinct().collect(Collectors.toList());
        //    System.out.println(list1);

        // 2. 过滤"伊"后收集成 Set 集合(Set 天然去重)
        //    TODO: Set<String> set = list.stream().filter(s -> s.contains("伊"))
        //                                   .collect(Collectors.toSet());
        //    System.out.println(set);

        // 3. 挑战:验证 toList() 返回的集合不可变——尝试 list1.add("旺财")
        //    TODO: 观察抛出的异常类型(UnsupportedOperationException)
    }
}

预期输出

========== distinct + toList ==========
[长贵, 赵四, 郑伊健, 陈浩南, 刘能]
========== filter + toSet ==========
[郑伊健, 陈浩南]
========== 挑战 ==========
Exception in thread "main" java.lang.UnsupportedOperationException

思考题

  1. collect 是中间操作还是终结操作?(提示:终结操作,执行完流结束,把结果收集到集合)
  2. Collectors 是什么?(提示:收集工具类,类比 Arrays / Collections)
  3. toList()toSet() 返回的集合有什么特点?(提示:toList 不可变;toSet 天然去重且无序)

第二部分:进阶挑战

以下练习综合运用 Stream 流 + 实体类 + Collectors 收集器。


进阶 1:Collectors.toMap 收集成 Map(学生信息映射)

难度:⭐⭐⭐⭐ 知识点Collectors.toMap(keyMapper, valueMapper)Function<T, R> 函数式接口、Lambda vs 匿名内部类、Map<Integer, Student>

场景描述:把流中的学生对象收集成 Map<Integer, Student>——键是学号(id),值是学生对象本身。toMap 的两个参数都是 Function 函数式接口:第一个从元素中取"键",第二个从元素中取"值"。

题目要求

  1. 定义 Student 实体类:

    import java.util.Objects;
    
    public class Student {
    private Integer id;   // 学号
    private String name;  // 姓名
    
    // TODO: 无参构造 + 有参构造(id, name)
    // TODO: getter / setter
    // TODO: equals / hashCode —— 基于 id 和 name(Objects.equals / Objects.hash)
    // TODO: toString
    }
    
  2. 编写测试类 StudentMapTest

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    
    public class StudentMapTest {
    public static void main(String[] args) {
        Student s1 = new Student(1001, "项羽");
        Student s2 = new Student(1002, "张良");
        Student s3 = new Student(1003, "项庄");
        Student s4 = new Student(1004, "刘邦");
        Student s5 = new Student(1005, "萧何");
        Student s6 = new Student(1006, "韩信");
        Student s7 = new Student(1007, "樊哙");
        ArrayList<Student> list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7));
    
        // 1. Lambda 写法:toMap(键, 值)
        //    TODO: Map<Integer, Student> map = list.stream()
        //              .collect(Collectors.toMap(
        //                  student -> student.getId(),   // 键:学号
        //                  student -> student            // 值:学生对象
        //              ));
        //    System.out.println(map);
    
        // 2. 匿名内部类写法:Function 函数式接口(与 Lambda 等价)
        //    TODO: Map<Integer, Student> map2 = list.stream()
        //              .collect(Collectors.toMap(
        //                  new Function<Student, Integer>() {
        //                      public Integer apply(Student student) { return student.getId(); }
        //                  },
        //                  new Function<Student, Student>() {
        //                      public Student apply(Student student) { return student; }
        //                  }
        //              ));
    
        // 3. 挑战:用 get(1003) 取出"项庄",输出学生姓名
        //    TODO: System.out.println(map.get(1003).getName());   // 期望 项庄
    }
    }
    

预期输出

{1001=Student{id=1001, name='项羽'}, 1002=Student{id=1002, name='张良'}, ...}
========== 按学号取值 ==========
项庄

原理分析(必答)

  1. Collectors.toMap 的两个参数分别是什么?(提示:keyMapper 取键、valueMapper 取值,都是 Function 函数式接口)
  2. student -> student.getId() 与匿名内部类 new Function<Student, Integer>(){...} 是什么关系?(提示:等价,Lambda 是后者的简化)
  3. 如果两个学生学号相同(id 重复),toMap 会发生什么?(提示:默认抛 IllegalStateException——键必须唯一)

进阶 2:综合实战——员工信息流式处理

难度:⭐⭐⭐⭐ 知识点:filter / distinct / count / collect / toMap 综合运用、contains() 判断、Stream 全流程

场景描述:某公司有一批员工数据(工号、姓名、部门),用 Stream 流完成以下任务:过滤出技术部员工、按姓名去重、统计人数、收集成 Map(键=工号,值=姓名)。

题目要求

  1. 定义 Employee 类:

    public class Employee {
    private String id;      // 工号
    private String name;    // 姓名
    private String dept;    // 部门
    
    public Employee(String id, String name, String dept) {
        this.id = id;
        this.name = name;
        this.dept = dept;
    }
    
    // TODO: getter / setter / toString
    }
    
  2. 编写测试类 EmployeeStreamTest

    import java.util.*;
    import java.util.stream.Collectors;
    
    public class EmployeeStreamTest {
    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>(List.of(
                new Employee("E001", "张三", "技术部"),
                new Employee("E002", "李四", "市场部"),
                new Employee("E003", "王五", "技术部"),
                new Employee("E004", "张三", "技术部"),   // 姓名重复,测试去重
                new Employee("E005", "赵六", "财务部")
        ));
    
        // 1. 过滤:只保留"技术部"的员工,遍历输出
        //    TODO: list.stream().filter(e -> "技术部".equals(e.getDept())).forEach(...)
    
        // 2. 去重:按姓名去重(先 map 取出姓名,再 distinct)
        //    TODO: list.stream().map(Employee::getName).distinct().forEach(System.out::println);
        //    期望:张三 王五 李四 赵六(注意:张三只出现一次)
    
        // 3. 统计:技术部员工个数(filter + count)
        //    TODO: long cnt = list.stream().filter(e -> "技术部".equals(e.getDept())).count();
    
        // 4. 收集:收集成 Map<String, String>,键=工号,值=姓名
        //    TODO: Map<String, String> map = list.stream()
        //              .collect(Collectors.toMap(Employee::getId, Employee::getName));
        //    System.out.println(map);
    }
    }
    

预期输出

========== 技术部员工 ==========
E001-张三-技术部
E003-王五-技术部
E004-张三-技术部
========== 按姓名去重 ==========
张三 王五 李四 赵六
========== 技术部人数统计 ==========
3
========== 收集成 Map(工号→姓名) ==========
{E001=张三, E002=李四, E003=王五, E004=张三, E005=赵六}

原理分析(必答)

  1. Employee::getName 是什么写法?等价于什么 Lambda?(提示:方法引用,等价于 e -> e.getName()
  2. 任务 4 中 toMap(Employee::getId, Employee::getName) 的两个参数分别是什么?(提示:键=工号、值=姓名,都是方法引用)
  3. 任务 3 为什么能"先 filter 再 count"统计人数?(提示:filter 是中间方法返回新流,count 是终结方法统计个数)

参考答案要点

练习 1 参考

// 2. Map 体系集合
Stream<String> keyStream = map.keySet().stream();    // key 就是 Set 集合
Stream<Object> valueStream = map.values().stream();  // value 就是 List 集合

// 3. 数组
Stream<String> arrayStream = Arrays.stream(array);

// 4. 相同类型的多个数据(Stream.of 本质是可变参数 T...)
Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
Stream<Integer> integerStream = Stream.of(10, 15, 20, 35, 46);

练习 2 参考

// 1. 以"万"开头
list.stream().filter(s -> s.startsWith("万")).forEach(System.out::println);

// 2. 以"万"开头 且 长度为 3
list.stream()
        .filter(s -> s.startsWith("万"))
        .filter(s -> s.length() == 3)
        .forEach(System.out::println);

// 3. 统计个数
long count = list.stream()
        .filter(s -> s.startsWith("万"))
        .filter(s -> s.length() == 3)
        .count();
System.out.println("个数:" + count);   // 3

练习 3 参考

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

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

// 3. 分页方法:skip((pageNo-1)*pageSize).limit(pageSize)
public void showPage(List<Integer> list, int pageNo, int pageSize) {
    list.stream()
        .skip((pageNo - 1) * pageSize)   // 跳过前面的页
        .limit(pageSize)                 // 截取本页数量
        .forEach(n -> System.out.print(n + " "));
    System.out.println();
}

练习 4 参考

// 1. concat 合并(含重复)
Stream.concat(streamA, streamB).forEach(System.out::println);
// 长贵 赵四 刘能 旺财 长贵 赵四

// 2. concat + distinct 去重
Stream.concat(streamA, streamB).distinct().forEach(System.out::println);
// 长贵 赵四 刘能 旺财

// 3. List 去重
list.stream().distinct().forEach(System.out::println);
// 长贵 赵四 郑伊健 陈浩南 刘能

练习 5 参考

// 1. forEach 遍历
list.stream().forEach(System.out::println);

// 2. count 计数
long count = list.stream().filter(s -> s.startsWith("万")).count();
System.out.println("以\"万\"开头的有:" + count + " 个");

// 3. 挑战:forEach 后再调 count() → 编译报错
// 原因:forEach 是终结方法,调用后流已结束,不能再继续链式操作
// list.stream().forEach(System.out::println).count();  // ✗ 编译错误

练习 6 参考

// 1. 去重后收集成 List
List<String> list1 = list.stream().distinct().collect(Collectors.toList());
System.out.println(list1);

// 2. 过滤"伊"后收集成 Set
Set<String> set = list.stream()
        .filter(s -> s.contains("伊"))
        .collect(Collectors.toSet());
System.out.println(set);

// 3. 挑战:toList() 返回不可变集合,add 抛 UnsupportedOperationException
// list1.add("旺财");  // ✗ 运行时抛 UnsupportedOperationException

进阶 1 参考(核心逻辑)

// Student:equals/hashCode 基于 id + name
@Override
public boolean equals(Object o) {
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return Objects.equals(id, student.id) && Objects.equals(name, student.name);
}

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

@Override
public String toString() {
    return "Student{id=" + id + ", name='" + name + "'}";
}

// StudentMapTest:Lambda 方式收集成 Map
Map<Integer, Student> map = list.stream()
        .collect(Collectors.toMap(
                student -> student.getId(),   // 键:学号
                student -> student            // 值:学生对象
        ));
System.out.println(map);
System.out.println(map.get(1003).getName());   // 项庄

进阶 2 参考(核心逻辑)

// 1. 过滤技术部
list.stream()
    .filter(e -> "技术部".equals(e.getDept()))
    .forEach(System.out::println);

// 2. 按姓名去重(map 取出姓名 → distinct 去重)
list.stream()
    .map(Employee::getName)
    .distinct()
    .forEach(System.out::println);

// 3. 统计技术部人数
long cnt = list.stream()
    .filter(e -> "技术部".equals(e.getDept()))
    .count();

// 4. 收集成 Map(键=工号,值=姓名)
Map<String, String> map = list.stream()
    .collect(Collectors.toMap(Employee::getId, Employee::getName));
System.out.println(map);

涵盖知识点总览

知识点 对应练习 说明
生成流的四种方式 练习 1 Collection.stream() / Map keySet/values / Arrays.stream / Stream.of
Stream.of 与可变参数 练习 1 of(T...values) 本质是可变参数,可传任意个数数据
filter 过滤(Predicate) 练习 2、3、5、6、进阶 2 传入断言条件,返回符合条件的新流
forEach 遍历(Consumer) 练习 2、5 对每个元素执行动作,方法引用 System.out::println
中间方法链式调用 练习 3 中间方法返回新流对象,可一直链式
limit 截取 / skip 跳过 练习 3 分页公式 skip((n-1)*size).limit(size)
concat 合并两流 练习 4 Stream 接口静态方法 Stream.concat(a, b)
distinct 去重 练习 4、6 依据 Object.equals(obj)
终结方法只能有一个 练习 5 forEach/count 调用后流结束
count 计数(long) 练习 5、进阶 2 返回元素个数,类型 long
collect 收集操作 练习 6、进阶 1、进阶 2 把流结果收集到集合,配合 Collectors
Collectors.toList() / toSet() 练习 6 toList 不可变;toSet 天然去重
Collectors.toMap(keyMapper, valueMapper) 进阶 1、进阶 2 两个 Function 参数:取键 / 取值
Function 函数式接口 进阶 1 有参有返回值;Lambda vs 匿名内部类
方法引用 类::方法 进阶 2 Employee::getName ≈ e -> e.getName()
Stream + 实体类综合 进阶 1、进阶 2 实体类 + equals/hashCode + 收集成 Map