CodeGym /课程 /JAVA 25 SELF /通过 Stream 转换集合

通过 Stream 转换集合

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

1. List → Set 的相互转换

回顾一下为什么需要转换集合。在实际场景中我们常常需要:

  • 从列表获取唯一元素(例如,e-mail 列表 → 唯一地址的集合)。
  • 构建映射(Map),例如从名字列表得到“名字 → 名字长度”的映射。
  • 将元素合并成一个字符串(例如用于美观输出)。

过去为此要写大量包含循环、条件和临时集合的代码。有了 Stream API,一切更简单也更优雅!

示例:从列表获取唯一名字的集合

假设我们有一个名字列表(也许有人在你的程序里把自己输了两次——很常见!):

List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");

我们的目标——得到一个每个名字只出现一次的集合,即 Set。借助 Stream API,几乎一行就能完成:

Set<String> uniqueNames = names.stream()
    .collect(Collectors.toSet());
System.out.println(uniqueNames);

输出:

[Maria, Ivan, Anna, Sergey]

(Set 中的顺序不保证——如果你的结果顺序不同,不要惊讶。)

如果需要反过来:Set → List?

有时需要把集合再变回列表(例如用来排序或按索引访问):

List<String> namesList = uniqueNames.stream()
    .collect(Collectors.toList());
System.out.println(namesList);

2. 转换为 Map:Collectors.toMap()

示例:从名字列表得到 Map “名字 → 名字长度”

有时我们不仅想当程序员,还想当“制图师”——来构建一张映射表!试试看:

List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");

Map<String, Integer> nameToLength = names.stream()
    .collect(Collectors.toMap(
        name -> name,         // 键 —— 名字本身
        name -> name.length() // 值 —— 名字长度
    ));

System.out.println(nameToLength);

输出:

{Maria=5, Ivan=4, Anna=4, Sergey=6}

重要点:键重复

如果源列表中存在相同的名字,尝试收集成 Map 会抛出 IllegalStateException: Duplicate key。Java 不允许你用同一个键放两个值。

如何处理重复键?
可以指定键冲突时如何处理——例如保留第一个值或最后一个值:

List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");

Map<String, Integer> nameToLength = names.stream()
    .collect(Collectors.toMap(
        name -> name,
        name -> name.length(),
        (oldValue, newValue) -> oldValue // 保留第一个值
    ));

System.out.println(nameToLength);

现在程序不会崩溃,且 Map 中只会保留每个名字的第一次出现。

示例:值为对象的 Map

稍微复杂一点:我们有一个用户列表,希望构建 Map “名字 → 用户”:

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

// 用户列表示例
List<User> users = List.of(
    new User("Anna", 25),
    new User("Sergey", 30),
    new User("Maria", 22)
);

Map<String, User> nameToUser = users.stream()
    .collect(Collectors.toMap(
        user -> user.name,
        user -> user
    ));

System.out.println(nameToUser);

输出:

{Maria=Maria (22), Anna=Anna (25), Sergey=Sergey (30)}

3. 拼接为字符串:Collectors.joining()

有时我们不止要收集集合,而是要生成一个漂亮的字符串用于用户输出或日志。例如,用逗号拼接所有名字:

List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");

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

System.out.println(result);

输出:

Anna, Sergey, Maria, Ivan

还能添加前缀和后缀

String result = names.stream()
    .collect(Collectors.joining(", ", "列表: [", "]"));

System.out.println(result);

输出:

列表: [Anna, Sergey, Maria, Ivan]

4. 终端操作:forEachcollectcountanyMatchallMatchnoneMatch

forEach 方法

我们已经很熟悉 forEach:该操作会对流中的每个元素执行一个动作。

names.stream().forEach(name -> System.out.println("你好," + name + "!"));

collect 方法

将元素收集为集合、字符串或其他结构。最常见的是通过 Collectors.toList()Collectors.toSet() 收集到 ListSet

count 方法

统计流中元素的数量。

long count = names.stream()
    .filter(name -> name.length() > 4)
    .count();
System.out.println("长度大于 4 的名字数量: " + count);

anyMatchallMatchnoneMatch 方法

检查条件是否对至少一个元素成立(anyMatch)、对所有元素成立(allMatch),或对任一元素都不成立(noneMatch)。

boolean hasShortName = names.stream()
    .anyMatch(name -> name.length() < 4);
System.out.println("是否存在短名字? " + hasShortName);

boolean allLong = names.stream()
    .allMatch(name -> name.length() > 3);
System.out.println("所有名字都长于 3 个字母? " + allLong);

boolean noneIvan = names.stream()
    .noneMatch(name -> name.equals("Ivan"));
System.out.println("没有 Ivan 吗? " + noneIvan);

输出:

是否存在短名字? false
所有名字都长于 3 个字母? true
没有 Ivan 吗? false

5. 终端操作与中间操作:概念巩固

中间操作filtermapdistinctsortedlimitskippeek)——返回新的 Stream,可以构建链式调用。

终端操作forEachcollectcountanyMatchallMatchnoneMatchreducefindFirstfindAny)——结束该流,之后将不再有结果!

链式调用示例:

List<String> result = users.stream()
    .filter(user -> user.age > 20)
    .map(user -> user.name.toUpperCase())
    .distinct()
    .sorted()
    .collect(Collectors.toList());

System.out.println(result);

输出:

[ANNA, IVAN, MARIA, SERGEY]

6. 通过 Stream 转换集合时的常见错误

错误 1:在 toMap 中未处理重复键
如果源集合中存在重复键,而你使用 Collectors.toMap() 时没有显式提供合并函数,程序会抛出异常。此类场景请务必提供合并函数:

// 保留最后一个值
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)

错误 2:用 forEach 代替 collect
有时新手会尝试用 forEach 来“收集”集合,例如:

List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // 能跑,但这不是 Stream 的用法!

更好的做法是使用 collect(Collectors.toList()) —— 更安全也更简洁。

错误 3:尝试重复使用同一个流
流只能使用一次。执行终端操作(例如 collectforEach)之后,尝试继续使用同一个 Stream 会导致抛出 IllegalStateException

错误 4:违反“无副作用”原则
中间操作应该是“纯”的(不修改外部变量)。不要在 mapfilter 中修改流之外的内容。

错误 5:忽略 SetMap 的顺序特性
如果元素顺序很重要,请使用相应的集合——例如 LinkedHashSetTreeMap——并指定合适的收集器。

1
调查/小测验
Stream API 基础第 30 级,课程 4
不可用
Stream API 基础
Stream API 基础
评论 (2)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
ncksllpo 级别 35,Cherkasy,Ukraine
25 二月 2026
这下方法的签名很重要吗?😥
ncksllpo 级别 35,Cherkasy,Ukraine
25 二月 2026
可以用distinct方法来去掉重复键吗