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 的操作分为两类。
- 中间操作(例如,filter、map、distinct)——描述处理的各个阶段。它们返回一个新的流,但本身不会触发执行。
- 终端操作(例如,collect、forEach、count)——触发流水线并产生结果。
流是“惰性”的:在没有调用终端操作之前,不会发生任何计算。因此我们经常以 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]
步骤:
- .stream() — 从列表创建流。
- .filter(name -> name.startsWith("A")) — 只保留以 "A" 开头的名字。
- .map(String::length) — 将每个名字映射为其长度。
- .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 很“懒”,就像窗台上的猫:在没有调用终端操作(例如 collect 或 forEach)之前,什么都不会发生。如果只写 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"))
GO TO FULL VERSION