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. 终端操作:forEach、collect、count、anyMatch、allMatch、noneMatch
forEach 方法
我们已经很熟悉 forEach:该操作会对流中的每个元素执行一个动作。
names.stream().forEach(name -> System.out.println("你好," + name + "!"));
collect 方法
将元素收集为集合、字符串或其他结构。最常见的是通过 Collectors.toList() 和 Collectors.toSet() 收集到 List 或 Set。
count 方法
统计流中元素的数量。
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("长度大于 4 的名字数量: " + count);
anyMatch、allMatch、noneMatch 方法
检查条件是否对至少一个元素成立(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. 终端操作与中间操作:概念巩固
中间操作(filter、map、distinct、sorted、limit、skip、peek)——返回新的 Stream,可以构建链式调用。
终端操作(forEach、collect、count、anyMatch、allMatch、noneMatch、reduce、findFirst、findAny)——结束该流,之后将不再有结果!
链式调用示例:
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:尝试重复使用同一个流
流只能使用一次。执行终端操作(例如 collect、forEach)之后,尝试继续使用同一个 Stream 会导致抛出 IllegalStateException。
错误 4:违反“无副作用”原则
中间操作应该是“纯”的(不修改外部变量)。不要在 map 或 filter 中修改流之外的内容。
错误 5:忽略 Set 和 Map 的顺序特性
如果元素顺序很重要,请使用相应的集合——例如 LinkedHashSet、TreeMap——并指定合适的收集器。
GO TO FULL VERSION