Java Stream

Java Stream 是 Java 8 引入的一个功能强大的工具,用于处理集合数据(如 List、Set 等)或数组的声明式操作。它提供了一种函数式编程风格的方式,让开发者能够以简洁、可读性高且高效的方式处理数据。Stream 的核心思想是将数据操作分为一系列中间操作和终端操作,通过链式调用实现复杂的处理逻辑,同时支持并行处理以提高性能。
1. Stream 的核心概念
Stream 的设计灵感来源于函数式编程,强调数据的不可变性和声明式操作。以下是 Stream 的核心概念:
Stream(流):
- Stream 是一个抽象的数据处理管道,表示数据的序列,但并不存储数据本身。
- Stream 可以看作是对数据源(如集合、数组)的“视图”,支持对其进行操作(如过滤、映射、排序等)。
- Stream 不修改原始数据源,操作的结果通常是生成新的 Stream 或最终的结果。
数据源:
- Stream 的数据源可以是集合(Collection,如 List、Set)、数组、文件、I/O 通道或其他数据结构。
- 常见创建 Stream 的方式:
List<String> list = Arrays.asList("apple", "banana", "cherry"); Stream<String> stream = list.stream(); // 从 List 创建 Stream Stream<String> arrayStream = Stream.of("apple", "banana", "cherry"); // 从数组创建
操作类型:
- 中间操作(Intermediate Operations): 对 Stream 进行处理并返回一个新的 Stream,允许链式调用。中间操作是惰性求值的(lazy evaluation),只有在终端操作触发时才会执行。
- 终端操作(Terminal Operations): 触发 Stream 的处理并返回最终结果(如一个值、集合或无返回值)。终端操作会“关闭” Stream,之后 Stream 不可再用。
并行处理:
- Stream 支持并行处理,通过
parallelStream()或parallel()方法可以利用多核处理器并行执行操作,适合处理大数据量。
- Stream 支持并行处理,通过
2. Stream 的操作分类
Stream 的操作分为两类: 中间操作和终端操作。以下是常见的操作及其功能:
2.1 中间操作
中间操作是惰性的,只有当终端操作触发时才会执行。常见中间操作包括:
filter(Predicate
) : 根据条件过滤 Stream 中的元素。List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream() .filter(s -> s.startsWith("a")) .forEach(System.out::println); // 输出: applemap(Function<T, R>): 将 Stream 中的元素映射为另一种类型或格式。
List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream() .map(String::toUpperCase) .forEach(System.out::println); // 输出: APPLE, BANANA, CHERRYflatMap(Function<T, Stream
>) : 将 Stream 中的每个元素映射为一个新的 Stream,并将所有 Stream 展平为一个 Stream。适用于“一对多”的场景。List<List<String>> nestedList = Arrays.asList( Arrays.asList("a", "b"), Arrays.asList("c", "d") ); nestedList.stream() .flatMap(Collection::stream) // 将多个 List 展平为一个 Stream .forEach(System.out::println); // 输出: a, b, c, dsorted() / sorted(Comparator
) : 对 Stream 中的元素进行排序。List<String> list = Arrays.asList("banana", "apple", "cherry"); list.stream() .sorted() .forEach(System.out::println); // 输出: apple, banana, cherrydistinct(): 去除 Stream 中的重复元素。
List<String> list = Arrays.asList("apple", "banana", "apple"); list.stream() .distinct() .forEach(System.out::println); // 输出: apple, bananalimit(long maxSize): 限制 Stream 中元素的数量。
List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream() .limit(2) .forEach(System.out::println); // 输出: apple, bananaskip(long n): 跳过 Stream 中的前 n 个元素。
List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream() .skip(1) .forEach(System.out::println); // 输出: banana, cherrypeek(Consumer
) : 对 Stream 中的每个元素执行操作,通常用于调试, 注意这个是中间操作。List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream() .peek(s -> System.out.println("Processing: " + s)) .filter(s -> s.startsWith("b")) .forEach(System.out::println); // 输出: // Processing: apple // Processing: banana // banana // Processing: cherry
2.2 终端操作
终端操作会触发 Stream 的处理并返回结果,常见终端操作包括:
forEach(Consumer
) : 对 Stream 中的每个元素执行操作。List<String> list = Arrays.asList("apple", "banana", "cherry"); list.stream().forEach(System.out::println);collect(Collector): 将 Stream 转换为集合或其他数据结构。
List<String> list = Arrays.asList("apple", "banana", "cherry"); List<String> result = list.stream() .filter(s -> s.length() > 5) .collect(Collectors.toList()); // 收集到 List,底层默认实现为 ArrayList,但不建议直接强转toList()(Java 16+): 直接将 Stream 转换为 List。
- 不可变性: 返回的是不可变集合,调用
add()或remove()会抛出UnsupportedOperationException。 - 底层实现: 底层返回的是
ImmutableCollections$ListN(OpenJDK 实现),不是ArrayList,强转会抛出ClassCastException。 - 允许 Null: 与
List.of()不同,Stream.toList()允许流中存在null元素。 - 线程安全: 由于不可变性,该集合是天然线程安全的。
- 性能特征: 创建速度快,但不支持高效的随机访问(如
ArrayList的get(index))。 - 与 List.of() 关系: 行为基本一致,都是返回不可变列表。
List<String> result = list.stream().toList();- 不可变性: 返回的是不可变集合,调用
reduce(T identity, BinaryOperator
) : 将 Stream 中的元素归约为一个值。List<Integer> numbers = Arrays.asList(1, 2, 3, 4); int sum = numbers.stream() .reduce(0, Integer::sum); // 求和,结果为 10count(): 返回 Stream 中元素的数量。
long count = list.stream().filter(s -> s.startsWith("a")).count(); // 统计以 "a" 开头的元素anyMatch(Predicate
) / allMatch(Predicate :) / noneMatch(Predicate ) - 检查 Stream 中是否存在/全部满足/全部不满足某个条件的元素。
boolean hasApple = list.stream().anyMatch(s -> s.equals("apple")); // truefindFirst() / findAny(): 查找 Stream 中的第一个元素或任意元素,返回 Optional。
findFirst(): 返回流中的第一个元素。在并行流中,为了保证“第一个”,性能开销较大。findAny(): 返回流中的任意一个元素。在并行流中性能更好,因为它不需要维护顺序。
Optional<String> first = list.stream().findFirst(); // Optional[apple]
2.3 短路操作
某些操作具有“短路”特性,即一旦满足条件,Stream 就会停止处理:
findFirst()、findAny(): 找到一个元素后立即结束。anyMatch()、allMatch()、noneMatch(): 满足条件后立即返回结果。limit(): 达到指定数量后停止处理。
2.4 数值流 (Primitive Streams)
为了避免频繁的装箱/拆箱(Boxing/Unboxing)带来的性能开销,Java 提供了专门处理基本类型的流: IntStream、LongStream 和 DoubleStream。
创建数值流:
IntStream intStream = IntStream.range(1, 5); // 生成 1, 2, 3, 4 IntStream closedStream = IntStream.rangeClosed(1, 5); // 生成 1, 2, 3, 4, 5对象流转数值流:
List<String> list = Arrays.asList("apple", "banana"); int totalLength = list.stream() .mapToInt(String::length) // 转换为 IntStream .sum();数值流转对象流:
Stream<Integer> boxed = IntStream.range(1, 5).boxed();
2.5 无限流 (Infinite Streams)
Stream 可以是无限的,通常配合 limit() 使用:
iterate:
Stream.iterate(0, n -> n + 2) .limit(5) .forEach(System.out::println); // 0, 2, 4, 6, 8generate:
Stream.generate(Math::random) .limit(3) .forEach(System.out::println);
3. Stream 的特性
惰性求值:
- 中间操作不会立即执行,只有当终端操作触发时,Stream 才会从头到尾处理数据。这种机制优化了性能,避免了不必要的计算。
- 示例:
list.stream() .filter(s -> { System.out.println("Filtering: " + s); return s.startsWith("a"); }) .forEach(System.out::println); // 只有调用 forEach 后,filter 才会执行
不可变性:
- Stream 操作不会修改原始数据源,结果通常是新的 Stream 或集合。
- 示例:
List<String> list = Arrays.asList("apple", "banana"); List<String> filtered = list.stream().filter(s -> s.startsWith("a")).toList(); // list 保持不变,filtered 是新列表
一次性使用:
- 一个 Stream 只能被消费一次,终端操作后 Stream 会关闭。如果需要再次使用,必须重新创建 Stream。
- 示例:
Stream<String> stream = list.stream(); stream.forEach(System.out::println); // stream.forEach(System.out::println); // 抛出 IllegalStateException
并行处理:
- 通过
parallelStream()或parallel(),Stream 可以并行处理数据,适合多核处理器环境。 - 示例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.parallelStream() .map(n -> n * 2) .forEach(System.out::println); // 顺序不保证
- 通过
4. Collectors 的使用
Collectors 类提供了丰富的工具,用于将 Stream 的结果收集到集合或其他数据结构中。常见用法包括:
收集到集合:
Collectors.toList(): 底层默认使用ArrayList。Collectors.toSet(): 底层默认使用HashSet。Collectors.toMap(): 底层默认使用HashMap。
List<String> list = Arrays.asList("apple", "banana", "cherry"); List<String> result = list.stream().collect(Collectors.toList()); // 默认 ArrayList,不建议强转 Set<String> set = list.stream().collect(Collectors.toSet()); // 默认 HashSet,不建议强转 Map<String, Integer> map = list.stream() .collect(Collectors.toMap(s -> s, String::length)); // 默认 HashMap,不建议强转分组:
Map<Integer, List<String>> byLength = list.stream() .collect(Collectors.groupingBy(String::length)); // {5=[apple], 6=[banana, cherry]}分区:
Map<Boolean, List<String>> partitioned = list.stream() .collect(Collectors.partitioningBy(s -> s.length() > 5)); // {false=[apple], true=[banana, cherry]}字符串拼接:
String joined = list.stream().collect(Collectors.joining(", ")); // apple, banana, cherry进阶收集器 (Downstream Collectors):
groupingBy支持嵌套收集器,实现多级分组或聚合:// 按长度分组后,统计每组的数量 Map<Integer, Long> countByLength = list.stream() .collect(Collectors.groupingBy(String::length, Collectors.counting())); // 按长度分组后,提取每组的第一个字符并收集到 Set Map<Integer, Set<Character>> firstCharByLength = list.stream() .collect(Collectors.groupingBy( String::length, Collectors.mapping(s -> s.charAt(0), Collectors.toSet()) ));收集到指定集合类型:
List<String> list = Arrays.asList("apple", "banana", "cherry"); // 收集到 ArrayList(有序集合) ArrayList<String> arrayList = list.stream() .collect(Collectors.toCollection(ArrayList::new)); // 收集到 LinkedList(链表集合) LinkedList<String> linkedList = list.stream() .collect(Collectors.toCollection(LinkedList::new)); // 收集到 TreeSet(有序集合,自动去重并排序) TreeSet<String> treeSet = list.stream() .collect(Collectors.toCollection(TreeSet::new)); // 收集到 PriorityQueue(优先队列) PriorityQueue<String> priorityQueue = list.stream() .collect(Collectors.toCollection(PriorityQueue::new));
5. 使用场景
Stream 适用于以下场景:
数据过滤和转换: 从集合中筛选出符合条件的元素或进行格式转换。
List<String> longNames = list.stream() .filter(s -> s.length() > 5) .map(String::toUpperCase) .toList();聚合计算: 求和、平均值、最大值、最小值等。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4); double average = numbers.stream() .mapToInt(Integer::intValue) .average() .orElse(0.0);分组和统计: 对数据进行分组或统计分析。
Map<Integer, Long> countByLength = list.stream() .collect(Collectors.groupingBy(String::length, Collectors.counting()));并行处理: 处理大数据量时利用多核处理器。
List<Integer> largeList = // 大数据量 largeList.parallelStream().map(n -> heavyComputation(n)).toList();
6. 注意事项
避免副作用:
- Stream 操作应尽量避免修改外部状态(如修改共享变量),否则可能导致线程安全问题。
- 错误示例:
推荐:List<String> result = new ArrayList<>(); list.stream().forEach(s -> result.add(s)); // 副作用,不推荐List<String> result = list.stream().toList();
并行 Stream 的谨慎使用:
- 并行 Stream 并不总是更快,适合 CPU 密集型任务(如计算)而非 I/O 密集型任务。
- 并行处理可能导致顺序不一致(如
forEach的输出顺序)。 - 示例:
list.parallelStream().forEach(System.out::println); // 输出顺序不定 - 线程池污染: 并行流默认使用
ForkJoinPool.commonPool()。如果在并行流中执行耗时的 I/O 操作,会阻塞该公共线程池,进而影响应用中其他使用该池的组件。
性能优化:
- 使用合适的操作顺序以减少计算量。例如,先
filter再map可以减少处理的数据量。list.stream() .filter(s -> s.length() > 5) // 先过滤 .map(String::toUpperCase) // 再转换 .toList();
- 使用合适的操作顺序以减少计算量。例如,先
异常处理:
- Stream 操作中的异常需要显式处理,通常结合
try-catch或 Optional。list.stream() .map(s -> { try { return someMethodThatMayThrow(s); } catch (Exception e) { return null; } }) .toList();
- Stream 操作中的异常需要显式处理,通常结合
避免在 Stream 中使用循环:
- Stream 的设计初衷是取代显式循环,尽量使用内置操作(如
map、filter)而非嵌套循环。
- Stream 的设计初衷是取代显式循环,尽量使用内置操作(如
正确使用 peek():
peek()主要用于调试,它是一个中间操作。如果没有终端操作,peek()不会执行。- 不要用
peek()来修改元素状态,这违反了函数式编程的无副作用原则。
不要依赖具体的集合实现进行强转:
- 虽然
Collectors.toList()、toSet()、toMap()目前在 OpenJDK 中分别返回ArrayList、HashSet、HashMap,但规范并未保证这一点。 - Java 16+ 的
Stream.toList()返回的是不可变列表,强转为ArrayList会导致ClassCastException。 - 如果明确需要特定的实现(如
ArrayList、TreeSet等),请显式使用.collect(Collectors.toCollection(ArrayList::new))或在toMap中指定 supplier。
- 虽然
7. 实际示例
以下是一个综合示例,展示 Stream 在处理复杂数据时的能力:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 25, "Female"),
new Person("Bob", 30, "Male"),
new Person("Charlie", 25, "Male"),
new Person("Diana", 35, "Female")
);
// 按年龄分组,统计每组人数
Map<Integer, Long> countByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
System.out.println("Count by age: " + countByAge);
// 筛选女性,转换为姓名大写,收集到 List
List<String> femaleNames = people.stream()
.filter(p -> p.getGender().equals("Female"))
.map(p -> p.getName().toUpperCase())
.toList();
System.out.println("Female names: " + femaleNames);
// 计算平均年龄
double averageAge = people.stream()
.mapToInt(Person::getAge)
.average()
.orElse(0.0);
System.out.println("Average age: " + averageAge);
}
}
class Person {
private String name;
private int age;
private String gender;
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getGender() { return gender; }
}输出:
Count by age: {25=2, 30=1, 35=1}
Female names: [ALICE, DIANA]
Average age: 28.758. List 收集方式对比
在 Java 中有多种将流转换为列表的方法,它们的行为差异如下:
| 特性 | Collectors.toList() | Collectors.toUnmodifiableList() | Stream.toList() (Java 16+) |
|---|---|---|---|
| 可变性 | 可变 (通常是 ArrayList) | 不可变 | 不可变 |
| 允许 Null | 是 | 否 (抛出 NPE) | 是 |
| 强转 ArrayList | 可以 (但不推荐) | 否 | 否 |
| 主要用途 | 通用收集 | 严格不可变且不含 null | 简洁、高效的不可变收集 |






