Stream流,这是一个非常重要的里程碑。它代表了从“命令式编程”向“声明式编程”风格的转变,刚开始不理解是非常正常的。
一、Stream是做什么的?—— “流水线”的比喻
你可以把Stream想象成一条工厂里的流水线。
- 数据源(集合、数组等):就像是待加工的原材料,被放在流水线的开头。
- 零个或多个“中间操作” (Intermediate operations):就像是流水线上的一道道加工工序,比如筛选、转换、排序等。这些工序不会立刻执行,只是被定义出来。
- 一个“终结操作” (Terminal operation):就像是流水线的最后一道工序,比如打包、装箱。只有到了这一步,整个流水线才会被启动,所有原材料才会依次经过各个加工工序,最终产生结果。
Stream的核心思想是:你只需告诉它“做什么”(What),而不是“怎么做”(How)。
- 传统循环(命令式):你写一个
for
循环,自己处理迭代、定义临时变量、写if
条件判断。你是在指导计算机每一步该怎么执行。 - Stream流(声明式):你直接说“帮我过滤出大于5的数,然后转换成字符串,最后排序并收集到一个列表里”。你只关心结果,而不关心内部是如何遍历和处理的。
举个例子:有一个数字列表,找出所有偶数,然后求它们的平方,最后收集到一个新列表里。
- 传统方式(命令式):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> evenSquares = new ArrayList<>(); for (Integer number : numbers) { if (number % 2 == 0) { // 自己写if判断 int square = number * number; // 自己计算平方 evenSquares.add(square); // 自己添加到新集合 } }
- Stream方式(声明式):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> evenSquares = numbers.stream() // 1. 获取流 .filter(n -> n % 2 == 0) // 2. “工序”一:过滤偶数 .map(n -> n * n) // 3. “工序”二:映射为平方 .collect(Collectors.toList()); // 4. “启动”流水线:收集结果
你看,Stream的代码更简洁、更易读,它的每一步操作都像在描述业务逻辑本身。
二、是任何类型都可以使用Stream吗?
不是任何类型本身都能用,但任何类型的【集合】或【数组】几乎都可以转换成Stream来处理。
Stream流本身是一个通用工具,但它不能直接作用于单个对象(比如你有一个String name = "Alice"
,你不能name.stream()
)。它的操作对象是数据序列。
数据源主要来自以下几个方面:
从集合(Collection)来(最常见):
- 所有
Collection
的实现类(List
,Set
,Queue
等)都有.stream()
方法。
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> streamFromList = list.stream(); Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3)); Stream<Integer> streamFromSet = set.stream();
- 所有
从数组来:
- 使用
Arrays.stream(array)
静态方法。
int[] numbers = {1, 2, 3, 4, 5}; IntStream streamFromArray = Arrays.stream(numbers); // 注意是IntStream
- 使用
使用Stream类的静态方法:
Stream.of(T... values)
: 直接用值创建流。Stream.iterate()
和Stream.generate()
: 创建无限流。
Stream<String> streamOfValues = Stream.of("Java", "Python", "C++"); // 创建一个无限的奇数流:1, 3, 5, 7... Stream<Integer> infiniteStream = Stream.iterate(1, n -> n + 2);
从文件等I/O资源来(比如
Files.lines()
):try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) { lines.forEach(System.out::println); }
特别注意:基本类型
- 直接操作基本类型(
int
,long
,double
)为了避免装箱拆箱的性能损耗,Java提供了特化的流:IntStream
,LongStream
,DoubleStream
。 - 它们有额外的方法,如
sum()
,average()
,range()
等。
- 直接操作基本类型(
所以,结论是:你需要有一个数据序列(通常是集合或数组),然后就可以把它变成Stream来使用。
三、Stream的操作分类:中间操作 vs. 终结操作
这是理解Stream执行机制的关键。Stream的操作分为两大类:
类型 | 特点 | 常见方法 | 返回值 |
---|---|---|---|
中间操作 | 懒惰的 (Lazy) | filter() , map() , sorted() , distinct() , limit() |
返回一个新的Stream |
不会立即执行,只是被记录下來 | flatMap() , peek() |
||
终结操作 | 积极的 (Eager) | collect() , forEach() , count() |
返回一个非Stream的结果 |
会触发整个流水线的实际执行 | findFirst() , anyMatch() , reduce() , min()/max() |
(如void, List, Optional, int等) |
执行原理:只有在调用终结操作时,所有中间操作才会组合成一个“流水线方案”,然后数据源中的元素会逐个地依次通过整个流水线。这种处理方式称为“循环融合”,效率很高,因为它只需要遍历一次集合。
四、常用的Stream操作(“加工工序”)
filter(Predicate<? super T> predicate)
- 过滤- 保留满足条件的元素。
Predicate
是一个返回boolean的函数。 .filter(s -> s.length() > 3)
// 保留长度大于3的字符串
- 保留满足条件的元素。
map(Function<? super T, ? extends R> mapper)
- 映射/转换- 将元素转换成另一种形式。
Function
是一个转换函数。 .map(String::toUpperCase)
// 将每个字符串转为大写.map(n -> n * 2)
// 将每个数字乘以2
- 将元素转换成另一种形式。
sorted()
/sorted(Comparator<? super T> comparator)
- 排序distinct()
- 去重limit(long maxSize)
- 限制数量collect(Collector<? super T, A, R> collector)
- 收集(最常用的终结操作)- 将流中的元素收集到各种不同的容器中(如List, Set, Map)。
.collect(Collectors.toList())
.collect(Collectors.toSet())
.collect(Collectors.joining(", "))
// 连接成字符串.collect(Collectors.groupingBy(User::getDepartment))
// 按部门分组
总结与建议
- Stream是什么:一个用于高效处理数据序列(特别是集合)的声明式API,遵循“流水线”模式。
- 核心优势:代码简洁、可读性强、易于并行化(只需将
.stream()
换成.parallelStream()
)。 - 使用条件:数据源需要是集合、数组等可以生成序列的类型。
- 关键机制:操作分为中间操作(懒惰,定义工序)和终结操作(积极,启动执行)。
给你的学习建议:
- 多练习从
List
和Array
创建流。 - 重点掌握
filter
、map
和collect
这三个最常用的方法。 - 理解每个中间操作都会返回一个新流,但不会触发计算。
- 记住,没有终结操作,整个Stream流水线就什么都不会做。