CodeGym /课程 /JAVA 25 SELF /Stream API 基础操作:map、filter、collect

Stream API 基础操作:map、filter、collect

JAVA 25 SELF
第 30 级 , 课程 1
可用

1. 创建流

要使用 Stream API,首先需要从某个集合或数组获取一个流。

创建流的示例

// 从列表
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// 从数组
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// 从若干值
Stream<String> letters = Stream.of("A", "B", "C");

简要说明:

  • list.stream() — 用于集合
  • Arrays.stream(array) — 用于数组
  • Stream.of(...) — 用于离散值

在我们应用中的示例

假设我们有一个用户列表:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

中间操作与终端操作

关键点:Stream API 的操作分为两类。

  • 中间操作(例如,filtermapdistinct)——描述处理的各个阶段。它们返回一个新的流,但本身不会触发执行。
  • 终端操作(例如,collectforEachcount)——触发流水线并产生结果。

流是“惰性”的:在没有调用终端操作之前,不会发生任何计算。因此我们经常以 collect(...) 结束链条——这正是流被收集回集合或其他结果的时刻。

2. filter 操作:按条件过滤元素

filter 是一种中间操作,它只保留满足给定条件的元素。

签名

Stream<T> filter(Predicate<? super T> predicate); 

Predicate 是一个函数式接口,接收元素并返回 true(保留)或 false(丢弃)。

示例:只保留偶数

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

发生了什么?

  • n -> n % 2 == 0 —— 一个 lambda 表达式,用于检查数字能否被 2 整除。
  • filter 只保留偶数。

示例:过滤以 “A” 开头的名字

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

重要提示: filter 不会修改集合——它会创建一个新的流,其中只包含需要的元素。

3. map 操作:把元素映射成其他内容

map 是一种转换操作。它会取出流中的每个元素,应用一个函数,并返回一个新元素。

签名

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function 是一个接口,接收一个元素并返回某个结果(可以是不同类型)。

示例:得到字符串的长度

List<String> names = List.of("Anna", "Boris", "Alex");

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

发生了什么?

  • map 把字符串转换为它的长度(name -> name.length())。
  • 结果是一个数字流。

示例:将字符串转换为大写

List<String> names = List.of("Anna", "Boris", "Alex");

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. collect 操作:将结果收集回集合

collect 是一个终端操作,也就是说它会结束流的处理,并把结果收集到集合或其他容器中。

签名

<R, A> R collect(Collector<? super T, A, R> collector)

别被这个可怕的签名吓到!在 99% 的情况下,你会使用 Collectors 类中的现成收集器。

Collectors 是一个工具类,提供了一组“收集器”。它告诉流要把结果收集成什么形式:列表、集合、字符串等。

示例:

  • Collectors.toList() — 收集为 List
  • Collectors.toSet() — 收集为 Set
  • Collectors.joining(", ") — 通过逗号连接成一个字符串

也就是说,Collectors 就像一组不同形状的盒子,你把流中的元素装进去。

示例:收集为 List

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

示例:收集为 Set

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

示例:用逗号连接字符串

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. 操作链:过滤 + 映射 + 收集结果

Stream API 的最大优势在于可以将多个操作串联起来。

示例:获取以 “A” 开头的名字的长度

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

步骤:

  1. .stream() — 从列表创建流。
  2. .filter(name -> name.startsWith("A")) — 只保留以 "A" 开头的名字。
  3. .map(String::length) — 将每个名字映射为其长度。
  4. .collect(Collectors.toList()) — 收集为列表。

等价的命令式代码

如果用“老方法”(命令式)来写,大致如下:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

对比一下:Stream API —— 一行就能表达,关注的是“做什么”,而不是“怎么做”。

6. 实战:几个小练习

来练练手!所有示例都可以在同一个文件中运行——只需替换数据即可。

练习 1:只保留奇数并将其平方

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

练习 2:从字符串列表中获取它们的首字母列表

List<String> names = List.of("Anna", "Boris", "Alex");

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

练习 3:过滤长度大于 3 的字符串并收集为 Set

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. 使用 filter、map、collect 时的常见错误

错误 № 1:忘记 collect——没有结果!
Stream API 很“懒”,就像窗台上的猫:在没有调用终端操作(例如 collectforEach)之前,什么都不会发生。如果只写 users.stream().filter(...).map(...); —— 是不会执行任何操作的。

错误 № 2:把 filter 和 map 的顺序搞反了
有时新手会先做 map,然后再 filter。例如,names.stream().map(String::length).filter(len -> len > 3) —— 这会得到数字而不是字符串。如果你需要长度大于 3 的字符串,请先过滤,再转换。

错误 № 3:忽略了不可变性
Stream API 的操作不会修改原始集合!它们返回一个新的结果。执行 List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); 之后,names 集合保持不变。

错误 № 4:试图使用外部可变列表
不要这样做:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

最好使用 collect —— 更安全也更简洁。

错误 № 5:NullPointerException
如果集合中可能包含 null 元素,在 null 上调用 name.startsWith("A") 会抛出异常。如果可能,请先过滤掉 null:

.filter(name -> name != null && name.startsWith("A"))
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION