跳到内容

元数据卡

  • 前置知识:第8+章(上)Lambda、函数式接口
  • 预计时间:20 分钟
  • 阅读模式:高度专注
  • 完成标志:能用 Stream API 完成"过滤-转换-排序-收集"流水线

你的进度

你学会了用 Lambda 把一段行为当作参数传递。但老陈师傅又给你出了个新难题。

村里有一份村民名单,要找出所有年龄大于 18 岁的,按名字排序,取出前 5 个,统一加上户籍前缀。

没有 Stream 的时候你会这样写:

java
// 老办法——嵌套循环,临时变量,容易出错
List<String> result = new ArrayList<>();
for (Villager v : villagers) {
    if (v.age() > 18) {
        result.add("[变量村] " + v.name());
    }
}
Collections.sort(result);
List<String> top5 = result.subList(0, Math.min(5, result.size()));

问题:6 行代码里塞满了循环控制、临时变量、索引维护。你要的不是"怎么过滤、怎么排序",而是"过滤掉未成年人、加上前缀、排序、取前 5 个"。现在的代码混淆了"想做什么"和"怎么做"。


第一幕:Stream 流水线

用 Stream 改写上面那段代码:

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

List<String> result = villagers.stream()
        .filter(v -> v.age() > 18)          // 过滤
        .map(v -> "[变量村] " + v.name())   // 转换
        .sorted()                            // 排序
        .limit(5)                            // 取前5
        .collect(Collectors.toList());       // 收成列表

语言:Java 8+ 预期输出:一个最多包含 5 条加了前缀的姓名字符串的列表

看明白了吗?没有临时列表,没有 for 循环的控制变量,没有中间索引操作。每一步声明式地描述"我想做什么"而不是"怎么做"。


第二幕:方法引用——更短的 Lambda

Lambda 已经很短了,但有时你只是调用一个已有的方法:

java
// Lambda 写法
Consumer<String> printer = s -> System.out.println(s);

// 方法引用——更短!
Consumer<String> printer = System.out::println;

双冒号 :: 是方法引用操作符。它等于"这个地方放一个方法引用,等我需要调的时候再调"。

四种常见形式:

java
// 1. 静态方法引用:类名::staticMethod
Function<String, Integer> parser = Integer::parseInt;
// 等效:s -> Integer.parseInt(s)

// 2. 实例方法引用(特定对象):对象::method
String city = "变量村";
Supplier<Integer> len = city::length;
// 等效:() -> city.length()

// 3. 实例方法引用(任意对象):类名::method
Function<String, Integer> getLen = String::length;
// 等效:s -> s.length()

// 4. 构造方法引用:类名::new
Supplier<List<String>> listMaker = ArrayList::new;
// 等效:() -> new ArrayList<>()

语言:Java 8+ 你试试:用 String::toUpperCase 创建一个 Function<String, String>,传入"hello"看看输出。

方法引用让代码读起来更像"我要在这里做这个操作"——语法噪音降到最低。


第三幕:中间操作 vs 终端操作

流操作分两类:

类别行为例子效果
中间操作返回新流,懒执行filtermapsortedlimitdistinct构建流水线,数据不动
终端操作触发执行,产生结果collectforEachcountreduceanyMatch真正取数据或消费

理解这一点很重要:终端操作之前,Stream 不会真正执行任何计算。

更多操作:

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

// distinct + sorted + count
long count = numbers.stream()
        .distinct()    // 去重 → [3,1,4,5,9,2,6]
        .sorted()      // 排序 → [1,2,3,4,5,6,9]
        .count();      // 终端操作 → 7
System.out.println(count);

// anyMatch / allMatch / noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);   // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0);    // true

// reduce——自定义"折叠"操作
int sum = numbers.stream().reduce(0, (a, b) -> a + b);     // 求和 → 31
int product = numbers.stream().reduce(1, (a, b) -> a * b); // 求积 → 6480

语言:Java 8+ 预期输出7truetrue316480


常见陷阱

陷阱一:Stream 只能消费一次

java
Stream<String> stream = messages.stream();
stream.forEach(System.out::println);
stream.count(); // IllegalStateException: stream has already been operated upon or closed

Stream 就像河里的水——流过去就流过去了。每次操作都要新建:messages.stream()

陷阱二:并行流慎用

java
// 简单粗暴加 .parallelStream()
list.parallelStream().forEach(item -> {
    counter++; // 竞态条件!线程不安全
});

初学阶段只用 stream(),不要碰 parallelStream()


旅人笔记

Stream 流水线 = filter → map → sorted → collect。 方法引用让 Lambda 再短一步。 中间操作只构建流水线,终端操作才触发执行。 Stream 只能消费一次。

下一站预告

Lambda 和 Stream 的理论学完了,但光看不练等于没学。下一站做一个完整的 Lambda/Stream 练习,把这些新技能焊进肌肉记忆里。

Built with VitePress | Software Systems Atlas