CodeGym /课程 /JAVA 25 SELF /使用 Stream API 的函数式风格

使用 Stream API 的函数式风格

JAVA 25 SELF
第 49 级 , 课程 3
可用

1. 命令式 vs 函数式风格

先从一个简单的问题开始:为什么需要“函数式风格”?它相比我们习惯的基于循环的写法有什么优势?在 Java 中,“函数式风格”到底指什么?

命令式风格

命令式风格是你告诉计算机如何一步一步地完成任务。比如,需要从字符串列表得到其长度列表,只保留奇数长度并按降序排序,你可能会这样写:

List<String> words = Arrays.asList("kot", "slon", "nosorog", "tigr", "mysh’");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

这里我们显式创建了中间列表、手动添加元素、排序——一切都按步骤来。

函数式风格

函数式风格是描述你想要什么结果,而不是如何实现。在 Java 中,这通过 Stream API 实现:

List<String> words = Arrays.asList("kot", "slon", "nosorog", "tigr", "mysh’");

List<Integer> result = words.stream()
    .map(String::length)
    .filter(len -> len % 2 != 0)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result); // [7, 5, 3]

这看起来就像在搭建一个数据处理“流水线”:先把单词变成长度(map),再过滤出奇数(filter),然后排序(sorted)。全部在一条链路中完成,没有显式的中间集合和循环。

对比:迭代式 vs 函数式风格

在传统的命令式写法中,我们写一个循环,在循环体中逐步告诉计算机该做什么:遍历每个元素、检查条件、添加到新列表或打印到控制台。代码能跑,但行数更多,任务越复杂,阅读和维护就越困难。

函数式风格让你描述目标而不是过程。我们不写冗长的循环,而是搭建操作链:过滤、转换、收集结果。这样更短、更直观,并且因为减少了对可变集合的“手工操作”,也更不容易出错。

当然也有另一面。对新手来说,这样的链式调用可能显得晦涩:多个 lambda 连在一起有时不如简单循环好读。所以函数式在简洁和表达力上占优,但也需要一点习惯和经验积累。

2. Stream API 的基础操作

Stream API 不是“新型循环”,而是一整套以函数式风格处理集合的工具箱。我们来看看核心操作。

如何得到一个 Stream?

List<String> list = List.of("a", "bb", "ccc");
Stream<String> stream = list.stream();

中间操作

  • map — 转换流中的元素
  • filter — 按条件过滤元素
  • flatMap — 将每个元素变成一个流并“展开”
  • sorted — 排序
  • distinct — 去重
  • limit / skip — 限制/跳过元素

终端操作

  • forEach — 对每个元素执行操作
  • collect — 将结果收集为集合
  • reduce — 将流归约为一个值(例如求和)
  • count — 统计元素数量
  • anyMatchallMatchnoneMatch — 条件检查

示例:处理链

List<String> names = List.of("Anna", "Boris", "Vika", "Gleb", "Dasha");

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

System.out.println(filtered); // [BORIS, VIKA, DASHA]

“流水线”的可视化示意:

[Anna, Boris, Vika, Gleb, Dasha]
   | filter (length>3)
[Boris, Vika, Dasha]
   | map (toUpperCase)
[BORIS, VIKA, DASHA]
   | sorted
[BORIS, VIKA, DASHA]
   | toList

每个操作都不会修改原始集合——而是产生一个新的流。

3. 流式执行与惰性求值

中间操作与终端操作

Stream 有两类操作。第一类是中间操作,比如 mapfiltersorted。它们返回一个新流,看似承诺要做什么,但实际上尚未执行。第二类是终端操作,比如 forEachcollectreduce。这些才会真正触发整个处理流程。关键点是,只要你没调用终端操作,流就保持“惰性”——计算不会开始。

示例:

Stream<String> stream = List.of("a", "bb", "ccc").stream()
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

System.out.println("调用 forEach 之前");
stream.forEach(System.out::println);

输出:

调用 forEach 之前
map: a
A
map: bb
BB
map: ccc
CCC

可以看到,在开始 forEach 之前,map 并不会执行。

为什么这很棒?

这种方式可以毫无负担地构建任意长的转换链。只有当确实需要时,流才会开始工作。由此我们可以轻松处理超大数据量,甚至是无限序列。而且惰性还帮助节省内存并更高效地执行计算。

4. 实战:任务“字符串 → 长度 → 只保留奇数 → 按降序”

我们一步步解决这个任务:“从字符串列表得到长度列表,只保留奇数长度,并按降序排序。”

命令式解法

List<String> words = Arrays.asList("kot", "slon", "nosorog", "tigr", "mysh’");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

使用 Stream API 的函数式解法

List<String> words = Arrays.asList("kot", "slon", "nosorog", "tigr", "mysh’");

List<Integer> result = words.stream()
    .map(String::length) // 将字符串转换为其长度
    .filter(len -> len % 2 != 0) // 只保留奇数长度
    .sorted(Comparator.reverseOrder()) // 按降序排序
    .toList(); // 收集为 List(Java 16+)

System.out.println(result); // [7, 5, 3]

说明:

  • map(String::length) — 针对每个字符串获取其长度。
  • filter(len -> len % 2 != 0) — 只保留奇数长度。
  • sorted(Comparator.reverseOrder()) — 按降序排序。
  • toList() — 将流收集为新列表。
类比

就像在工厂搭建传送带:每个阶段对零件进行新的处理,直到最后才把所有东西装进盒子。

5. 更多示例:map、filter、forEach、collect

示例 1:过滤并打印

List<String> names = List.of("Anna", "Boris", "Vika", "Gleb", "Dasha");

names.stream()
    .filter(name -> name.contains("a"))
    .forEach(System.out::println);
// 将输出:Anna, Dasha

示例 2:转换并收集到 Set

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

System.out.println(upperNames); // [ANNA, BORIS, VIKA, GLEB, DASHA]

示例 3:获取所有字符串长度之和

int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

System.out.println("总长度: " + totalLength);

示例 4:使用 Predicate 和 Function

Predicate<String> longName = name -> name.length() > 4;
Function<String, String> greet = name -> "你好," + name + "!";

names.stream()
    .filter(longName)
    .map(greet)
    .forEach(System.out::println);
// 你好,Boris!
// 你好,Dasha!

6. 使用 Stream API 的风格要点

避免可变状态

Stream API 倡导“纯函数”——没有副作用。这意味着不要在 lambda 中修改外部变量。

不推荐:

List<String> result = new ArrayList<>();
names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(result::add); // side-effect!

更好:

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

组合操作

可以把 mapfiltersorted 等方法组合成很长的链。关键是别滥用:如果链长到超出一屏,可能需要拆分成若干部分。

惰性求值

在到达终端操作之前,Stream API 不会做任何事。这有助于节省资源并构建高效的数据处理流水线。

不修改原始集合

Stream 不会修改原集合!所有变换都会返回新的流/集合。

7. 何时使用 Stream API

Stream API 很适合在以下场景:

  • 需要快速处理集合(过滤、转换、排序)。
  • 希望代码简洁、可读。
  • 不想手动创建中间集合。
  • 希望轻松增加并行性(通过 parallelStream())。

命令式风格有时更合适,例如:

  • 需要复杂逻辑,有多个嵌套循环和条件。
  • 在关键路径上追求极致性能(Stream API 有时稍慢)。
  • 需要处理可变状态(例如“就地”更新元素)。

8. 使用 Stream API 的常见错误

错误 1:用 forEach 来收集集合。 很多新手用 forEach 向新集合添加元素。这不是函数式风格!请改用 collecttoList()

错误 2:过早优化。 不要一上来就用 parallelStream() —— 只有在数据确实很大且任务是 CPU 密集型时,才需要并行。

错误 3:将 Stream API 与可变集合混用。 Stream API 倾向于不变数据。不要在 lambda 中修改集合元素。

错误 4:结果被“丢失”。 忘了调用终端操作——什么也不会发生。

错误 5:lambda 过于复杂。 如果一个 lambda 超过一两行,请把它提取到具名方法中。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION