一、 什么是 Stream 流?
Stream 是 Java 8 引入的一个强大的抽象概念,用于处理数据集合(尤其是 Collection 类型的数据)。它不同于传统的 Collection(如 List、Set),Stream 本身不存储数据,而是对数据源(如集合、数组)进行复杂操作(如过滤、映射、排序、聚合)的管道。你可以把它想象成一条流水线,数据像水流一样通过各个处理环节。
二、Stream 的核心特点
- 链式操作: 可以将多个操作(如
filter、map、sorted)连接起来形成一个操作流水线。 - 延迟执行: 中间操作(如
filter、map)只是记录操作步骤,并不会立即执行,直到遇到终端操作(如collect、forEach)才会触发整个流水线的执行。 - 内部迭代: 遍历操作由
StreamAPI 在内部完成,开发者只需关注"做什么",而不是"怎么做"。 - 函数式风格: 大量使用 Lambda 表达式作为参数,代码更简洁。
- 支持并行: 可以非常容易地转换为并行流以利用多核处理器提升性能。
三、Stream 操作的分类
1.获取流
一、从集合获取
通过集合类的 stream() 方法:
java
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream(); // 获取顺序流
Stream<String> parallelStream = list.parallelStream(); // 获取并行流
二、从数组获取
使用 Arrays.stream():
java
int[] array = {1, 2, 3};
IntStream intStream = Arrays.stream(array); // 基本类型数组
String[] strArray = {"x", "y"};
Stream<String> strStream = Arrays.stream(strArray); // 对象数组
使用Stream类提供的方法:Stream.of(T...values)
java
import java.util.stream.Stream;
public class StreamOfExample {
public static void main(String[] args) {
// 示例 1: 创建一个包含字符串的流
Stream<String> stringStream = Stream.of("Java", "Python", "C++", "JavaScript");
stringStream.forEach(System.out::println); // 输出: Java Python C++ JavaScript
// 示例 2: 创建一个包含整数的流 (注意: 这里传入的是Integer对象,不是int基本类型)
Stream<Integer> integerStream = Stream.of(10, 20, 30, 40);
integerStream.forEach(num -> System.out.println(num * 2)); // 输出: 20 40 60 80
// 示例 3: 创建一个包含多个元素的流 (甚至可以是不同类型的对象,只要它们有共同父类/接口)
// 这里所有元素都是Number的子类
Stream<Number> numberStream = Stream.of(100, 3.14, 200L);
numberStream.forEach(System.out::println); // 输出: 100 3.14 200
}
}
三、通过生成器创建
-
Stream.generate()生成无限流:
javaStream<Double> randomStream = Stream.generate(Math::random).limit(5); // 5个随机数 -
Stream.iterate()生成迭代流:
javaStream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(10); // 0,2,4,...,18
注意事项
- 所有操作需在终端操作(如
collect(),forEach())触发前完成- 流只能被消费一次 ,重复使用会抛出
IllegalStateException
2.中间操作(Intermediate Operations)
中间方法(intermediate operations)是流操作的核心部分,它们用于在数据流上执行各种转换和处理。这些方法返回一个新的 Stream,允许链式调用,并且是惰性求值的(lazy evaluation),这意味着它们不会立即执行,直到终端操作(如 collect 或 forEach)被调用时才会触发实际计算。常见的中间操作有:
filter(Predicate<T> predicate):过滤元素,保留满足条件的元素。map(Function<T, R> mapper):将元素转换成另一种形式或提取信息。flatMap(Function<T, Stream<R>> mapper):将每个元素转换成一个Stream,然后将所有Stream连接成一个Stream。distinct():去除重复元素(基于equals)。
如果希望自定义对象能够去重复,重写对象的hashCode和equals方法才可以
sorted()/sorted(Comparator<T> comparator):排序。peek(Consumer<T> action):对每个元素执行操作,主要用于调试。limit(long maxSize):截取前 N 个元素。skip(long n):跳过前 N 个元素。
这些方法可以组合使用,形成链式操作,实现复杂的数据处理。
java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamIntermediateExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 2, 4, 6);
// 链式调用中间方法:过滤偶数、映射为平方、去重、排序
Stream<Integer> processedStream = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤出偶数
.map(n -> n * n) // 计算平方
.distinct() // 去除重复项
.sorted(); // 自然顺序排序
// 终端操作:收集结果到列表
List<Integer> result = processedStream.toList();
System.out.println(result); // 输出: [4, 16, 36, 64, 100]
}
}
解释:
filter保留偶数(2, 4, 6, 8, 10)。map计算每个偶数的平方(4, 16, 36, 64, 100)。distinct去除重复的平方值(例如原始列表有重复的 2 和 4,但平方后 4 和 16 可能重复,但在此例中不重复)。sorted对结果排序(输出有序列表)。- 注意:所有中间方法都是惰性的,只有在
toList()终端操作被调用时才会执行。
重要注意事项
- 惰性求值 :中间方法不会立即执行计算,它们只是构建操作链。实际计算在终端操作(如
collect、forEach)时发生,这提高了效率。- 链式调用:可以连续使用多个中间方法,但每个方法都返回新流,原始流不会被修改。
- 无状态 vs 有状态 :大多数方法(如
filter、map)是无状态的,但distinct、sorted、limit和skip是有状态的,它们可能影响性能或顺序。- 并行流支持 :中间方法在并行流(
parallelStream())中也能工作,但需注意线程安全。
3.终端操作(Terminal Operations)
终结方法是 Stream 流水线的最后一步,它们会触发流的实际计算 ,并产生一个结果(或副作用),同时消耗该流。一旦调用终结方法,该流就不能再被使用。常见的终端操作有:
forEach(Consumer<T> action):遍历每个元素。toArray():将元素收集到数组中。reduce(...):将元素组合起来产生一个值(如求和、求最大值)。collect(Collector<T, A, R> collector):将元素收集到一个容器中(如List、Set、Map)。这是最常用、最强大的终端操作。min(Comparator<T> comparator)/max(Comparator<T> comparator):找最小/最大值。count():统计元素个数。anyMatch(Predicate<T> predicate)/allMatch/noneMatch:检查是否存在/所有/没有元素满足条件。findFirst()/findAny():找到第一个/任意一个元素。
1)遍历与消费
这些方法主要用于遍历流中的元素并执行操作,不返回流本身的值,而是产生副作用或直接结束。
-
void forEach(Consumer<? super T> action)-
功能 :对流中的每个元素执行给定的操作
action。 -
特点 :
- 不保证顺序(尤其在并行流中)。
- 通常用于产生副作用(如打印、修改外部状态)。
-
示例 :
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream().forEach(System.out::println); // 打印每个名字
-
-
void forEachOrdered(Consumer<? super T> action)-
功能 :对流中的每个元素执行给定的操作
action,并保证按照流的遭遇顺序执行(如果流有定义顺序)。 -
特点 :
- 在并行流中,虽然元素可能由不同线程处理,但
action的执行会严格按照流中元素的顺序进行,这可能会牺牲一些并行性能。 - 适用于需要严格顺序的副作用操作。
- 在并行流中,虽然元素可能由不同线程处理,但
-
示例 :
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.parallelStream().forEachOrdered(System.out::println); // 保证打印顺序是 Alice, Bob, Charlie
-
2)收集结果
这些方法将流中的元素聚合并生成一个结果,如集合、值或其他数据结构。
-
<R, A> R collect(Collector<? super T, A, R> collector)-
功能 :使用
Collector策略对元素进行可变归约操作。这是最强大、最常用的终结方法之一。 -
特点 :
Collector指定了如何累积元素、如何组合中间结果以及最终如何将结果转换为想要的类型。- 常用
Collectors工具类提供了许多预定义的收集器(如toList(),toSet(),toMap(),joining()等)。
-
示例 :
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> list = names.stream().collect(Collectors.toList()); // 收集到 List Set<String> set = names.stream().collect(Collectors.toSet()); // 收集到 Set String joined = names.stream().collect(Collectors.joining(", ")); // 用逗号连接成字符串 Map<Integer, List<String>> byLength = names.stream().collect(Collectors.groupingBy(String::length)); // 按字符串长度分组
-
-
Object[] toArray()-
功能 :将流中的元素收集到一个对象数组中。
-
示例 :
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Object[] array = names.stream().toArray();
-
-
<A> A[] toArray(IntFunction<A[]> generator)-
功能 :使用提供的数组构造器(
generator)创建一个类型正确的数组,并将流中的元素收集进去。 -
示例 :
javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String[] stringArray = names.stream().toArray(String[]::new); // 创建并填充 String 数组
-
3)聚合与计算
这些方法对流中的元素进行统计或计算,返回一个单一结果。
-
Optional<T> reduce(BinaryOperator<T> accumulator)-
功能 :使用关联的累加器函数
accumulator对流中的元素执行归约 操作。返回一个Optional(因为流可能为空)。 -
原理 :相当于
((a1 op a2) op a3) op ...。 -
示例 (求最大值):
javaOptional<Integer> max = Stream.of(1, 2, 3, 4, 5) .reduce(Integer::max); // Optional[5]
-
-
T reduce(T identity, BinaryOperator<T> accumulator)-
功能 :提供一个初始值
identity,然后使用累加器accumulator进行归约。即使流为空,也返回identity。 -
原理 :相当于
identity op a1 op a2 op a3 op ...。 -
示例 (求和):
javaint sum = Stream.of(1, 2, 3, 4, 5) .reduce(0, Integer::sum); // 15
-
-
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)-
功能 :更通用的归约形式。适用于结果类型
U与元素类型T不同的情况,或在并行流中需要指定组合器combiner来合并中间结果。 -
示例 (连接字符串):
javaString concatenated = Stream.of("a", "b", "c") .reduce("", (s, str) -> s + str, (s1, s2) -> s1 + s2); // "abc"
-
-
Optional<T> min(Comparator<? super T> comparator)-
功能 :根据提供的比较器
comparator返回流中的最小值 (Optional)。 -
示例 :
javaOptional<String> shortest = Stream.of("apple", "banana", "orange") .min(Comparator.comparingInt(String::length)); // Optional["apple"]
-
-
Optional<T> max(Comparator<? super T> comparator)-
功能 :根据提供的比较器
comparator返回流中的最大值 (Optional)。 -
示例 :
javaOptional<String> longest = Stream.of("apple", "banana", "orange") .max(Comparator.comparingInt(String::length)); // Optional["banana"]
-
-
long count()-
功能 :返回流中元素的数量。
-
示例 :
javalong count = Stream.of(1, 2, 3, 4, 5).count(); // 5
-
4)匹配与查找
这些方法用于检查流中的元素是否满足某些条件,或查找特定元素。
-
boolean allMatch(Predicate<? super T> predicate)-
功能 :检查流中的所有元素 是否都满足给定的谓词
predicate(全称量词 )。这是一个短路操作(遇到不满足的元素立即返回)。 -
示例 :
javaboolean allEven = Stream.of(2, 4, 6, 8).allMatch(n -> n % 2 == 0); // true boolean allEven2 = Stream.of(2, 4, 5, 8).allMatch(n -> n % 2 == 0); // false (遇到5就停了)
-
-
boolean anyMatch(Predicate<? super T> predicate)-
功能 :检查流中是否至少有一个元素 满足给定的谓词
predicate(存在量词 )。这是一个短路操作(遇到满足的元素立即返回)。 -
示例 :
javaboolean hasEven = Stream.of(1, 3, 5, 7).anyMatch(n -> n % 2 == 0); // false boolean hasEven2 = Stream.of(1, 3, 4, 5).anyMatch(n -> n % 2 == 0); // true (遇到4就停了)
-
-
boolean noneMatch(Predicate<? super T> predicate)-
功能 :检查流中是否没有任何元素 满足给定的谓词
predicate(等价于!anyMatch(predicate))。这是一个短路操作(遇到满足的元素立即返回)。 -
示例 :
javaboolean noEven = Stream.of(1, 3, 5, 7).noneMatch(n -> n % 2 == 0); // true boolean noEven2 = Stream.of(1, 3, 4, 5).noneMatch(n -> n % 2 == 0); // false (遇到4就停了)
-
-
Optional<T> findFirst()-
功能 :返回描述流中第一个元素 的
Optional(如果流非空)。对于有顺序的流,结果是确定的。这是一个短路操作。 -
示例 :
javaOptional<Integer> first = Stream.of(1, 2, 3).findFirst(); // Optional[1]
-
-
Optional<T> findAny()-
功能 :返回描述流中某个元素 的
Optional(如果流非空)。在并行流中,它可能返回任何线程最先处理完的元素,性能可能优于findFirst()。这是一个短路操作。 -
示例 :
javaOptional<Integer> any = Stream.of(1, 2, 3).findAny(); // 可能是 Optional[1], Optional[2], Optional[3] 中的任何一个
-
4.收集stream流
Java 8 引入的 Stream API 提供了一种高效处理集合数据的方式。collect() 方法是 Stream 操作中的一个关键终端操作,用于将流中的元素收集 (或汇聚)成各种形式的结果,例如集合、数组或聚合值。
1. 核心方法:collect()
collect() 方法有两个主要重载形式:
1) <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
supplier: 一个函数,用于创建并返回一个新的结果容器(例如,一个新的ArrayList)。accumulator: 一个函数,用于将流中的单个元素合并到结果容器中。combiner: 一个函数(用于并行流),用于将两个结果容器合并成一个(例如,将两个ArrayList合并成一个)。- 这种方式较为底层,允许你自定义收集逻辑。
2) <R, A> R collect(Collector<? super T, A, R> collector) (更常用)
- 接受一个
Collector接口的实现。 java.util.stream.Collectors工具类提供了大量预定义的、常用的收集器 (Collector实现)。
2. 使用 Collectors 工具类收集
Collectors 提供了多种静态工厂方法,用于创建常见的收集器:
1) 收集到 List (toList())
java
List<String> list = stream.collect(Collectors.toList());
// 在 Java 16+ 中,可以直接使用 stream.toList()
2) 收集到 Set (toSet())
java
Set<String> set = stream.collect(Collectors.toSet()); // 自动去重
3) 收集到指定的集合类型 (toCollection())
java
LinkedList<String> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));
4) 收集到 Map (toMap())
-
最基本的
toMap需要两个函数:一个生成键 (keyMapper),一个生成值 (valueMapper)。javaMap<Integer, String> map = stream.collect(Collectors.toMap( Person::getId, // 键: 从Person对象中提取id Person::getName // 值: 从Person对象中提取name )); -
处理键冲突:如果键可能重复,需要提供第三个参数 (
mergeFunction) 来解决冲突。javaMap<String, Integer> map = stream.collect(Collectors.toMap( s -> s.substring(0, 1), // 键: 字符串的首字母 s -> s.length(), // 值: 字符串长度 (oldValue, newValue) -> oldValue // 冲突时保留旧值 (或 newValue, 或相加 oldValue + newValue) )); -
指定具体的
Map实现类:使用第四个参数 (mapSupplier)。javaMap<String, Integer> treeMap = stream.collect(Collectors.toMap( ..., ..., ..., TreeMap::new ));
3. 示例代码
java
import java.util.*;
import java.util.stream.*;
public class StreamCollectionExample {
public static void main(String[] args) {
// 示例流
Stream<String> stringStream = Stream.of("Apple", "Banana", "Orange", "Grape", "Kiwi");
// 1. 收集到 List
List<String> fruitList = stringStream.collect(Collectors.toList());
System.out.println("List: " + fruitList); // 输出: List: [Apple, Banana, Orange, Grape, Kiwi]
// 重新生成流 (因为流是消耗性的)
stringStream = Stream.of("Apple", "Banana", "Orange", "Grape", "Kiwi");
// 2. 收集到 Set (自动去重,这里无重复)
Set<String> fruitSet = stringStream.collect(Collectors.toSet());
System.out.println("Set: " + fruitSet); // 输出顺序可能不同
// 重新生成流
stringStream = Stream.of("Apple", "Banana", "Orange", "Grape", "Kiwi", "Apple");
// 3. 收集到指定的集合类型 (LinkedList)
LinkedList<String> fruitLinkedList = stringStream.collect(Collectors.toCollection(LinkedList::new));
System.out.println("LinkedList: " + fruitLinkedList); // 输出: LinkedList: [Apple, Banana, Orange, Grape, Kiwi, Apple]
// 使用 Person 对象流示例
Stream<Person> personStream = Stream.of(
new Person(1, "Alice", 30),
new Person(2, "Bob", 25),
new Person(3, "Charlie", 35)
);
// 4. 收集到 Map (ID -> Name)
Map<Integer, String> idToNameMap = personStream.collect(Collectors.toMap(
Person::getId,
Person::getName
));
System.out.println("ID -> Name Map: " + idToNameMap); // 输出: {1=Alice, 2=Bob, 3=Charlie}
}
}
四、使用示例
假设有一个字符串列表,我们要过滤出以 "A" 开头的字符串,把它们转换成大写,并收集到一个新列表中:
java
List<String> names = Arrays.asList("Alice", "Bob", "Anna", "David", "Alex");
List<String> result = names.stream() // 1. 获取流 (顺序流)
.filter(name -> name.startsWith("A")) // 2. 中间操作:过滤
.map(String::toUpperCase) // 3. 中间操作:映射 (转为大写)
.collect(Collectors.toList()); // 4. 终端操作:收集到 List
System.out.println(result); // 输出: [ALICE, ANNA, ALEX]
五、并行流
将 .stream() 改为 .parallelStream() 即可获得并行流。处理大数据集时,并行流可能更快,但需要注意线程安全和操作是否适合并行(如某些有状态的操作)。
java
List<String> parallelResult = names.parallelStream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
六、注意事项
- 流只能消费一次: 一个
Stream对象只能进行一次终端操作。 - 避免副作用: 在
Stream操作(尤其是并行流)中修改外部状态可能导致并发问题,应尽量使用无状态的 Lambda 表达式。 - 空指针: 注意流中元素可能为
null,处理时要小心。 - 性能: 对于小数据集,顺序流可能比并行流更快,因为并行有额外的开销。
七、扩展:基本类型流
为了避免自动装箱/拆箱的开销,Java 提供了专门的流:
IntStream(用于int、short、char、byte)LongStream(用于long)DoubleStream(用于double、float) 它们有专门的方法(如sum()、average())。