Java Stream 是 Java 8 引入的一个功能强大的工具,用于处理集合数据(如 List、Set 等)或数组的声明式操作。它提供了一种函数式编程风格的方式,让开发者能够以简洁、可读性高且高效的方式处理数据。Stream 的核心思想是将数据操作分为一系列中间操作和终端操作,通过链式调用实现复杂的处理逻辑,同时支持并行处理以提高性能。

Stream 惰性执行:逐元素穿过 filter、map、limit 的实际轨迹

上图中的每一个环节都只是对同一条数据流追加处理规则;只有遇到终端操作时,元素才会真正从数据源出发,依次穿过这些环节。

核心概念速查

概念作用典型方法 / 示例关键点
数据源提供待处理的元素list.stream()Stream.of(...)Arrays.stream(array)Stream 不保存数据,数据仍属于原始集合或其他来源
中间操作描述如何加工元素filtermapsorted返回新的 Stream,可继续链式调用;默认惰性执行
终端操作触发计算并产出结果toListcollectcountforEach执行后该 Stream 不能再次使用
短路操作条件满足时尽早结束limitfindFirstanyMatch可减少不必要的元素处理
并行流将部分计算并行执行parallelStream()parallel()需要结合任务性质和实际压测决定是否使用

Stream 的核心概念

Stream 的设计灵感来源于函数式编程,强调数据的不可变性和声明式操作。以下是 Stream 的核心概念:

  • Stream(流):

    • Stream 是一个抽象的数据处理管道,表示数据的序列,但并不存储数据本身。
    • Stream 可以看作是对数据源(如集合、数组)的“视图”,支持对其进行操作(如过滤、映射、排序等)。
    • Stream 不修改原始数据源,操作的结果通常是生成新的 Stream 或最终的结果。
  • 数据源:

    • Stream 的数据源可以是集合(Collection,如 List、Set)、数组、文件、I/O 通道或其他数据结构。
    • 常见创建 Stream 的方式:
      1
      2
      3
      4
      List<String> list = Arrays.asList("apple", "banana", "cherry");
      Stream<String> stream = list.stream(); // 从 List 创建 Stream
      String[] fruits = {"apple", "banana", "cherry"};
      Stream<String> arrayStream = Arrays.stream(fruits); // 从数组创建(详见“数组与 Stream”一节)
  • 操作类型:

    • 中间操作(Intermediate Operations): 对 Stream 进行处理并返回一个新的 Stream,允许链式调用。中间操作是惰性求值的(lazy evaluation),只有在终端操作触发时才会执行。
    • 终端操作(Terminal Operations): 触发 Stream 的处理并返回最终结果(如一个值、集合或无返回值)。终端操作会“关闭” Stream,之后 Stream 不可再用。
  • 并行处理:

    • Stream 支持并行处理,通过 parallelStream()parallel() 方法可以利用多核处理器并行执行操作,适合处理大数据量。

Stream 的操作分类

Stream 的操作分为两类: 中间操作和终端操作。以下是常见的操作及其功能:

目标优先选择输出类型说明
保留满足条件的元素filterStream<T>一个元素最多保留一次
一对一转换元素mapStream<R>例如把字符串转换为大写
一对多转换并展平flatMapStream<R>例如把多个列表合并为一条流
去重、排序或截断distinctsortedlimitskipStream<T>都属于中间操作,可继续拼接
收集为容器toListcollectListSetMap根据是否需要可变性或具体集合类型选择
汇总为单个结果reducecountsumaverage值或 Optional终端操作会触发整条管道

中间操作

中间操作是惰性的,只有当终端操作触发时才会执行。常见中间操作包括:

  • filter(Predicate): 根据条件过滤 Stream 中的元素。

    1
    2
    3
    4
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    list.stream()
    .filter(s -> s.startsWith("a"))
    .forEach(System.out::println); // 输出: apple
  • map(Function<T, R>): 将 Stream 中的元素映射为另一种类型或格式。

    1
    2
    3
    4
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    list.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println); // 输出: APPLE, BANANA, CHERRY
  • flatMap(Function<T, Stream>): 将 Stream 中的每个元素映射为一个新的 Stream,并将所有 Stream 展平为一个 Stream。适用于“一对多”的场景。

    1
    2
    3
    4
    5
    6
    7
    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, d

    map 与 flatMap:子流是否被展开及对应的结果类型

    当映射函数返回集合、数组或 Stream 时,不要只记“flatMap 能压平”:重点是它会把每个子流中的元素逐个接入结果流,因此后续的 filtersortedcollect 面对的是 a、b、c、d,而不是两个嵌套流。

  • sorted() / sorted(Comparator): 对 Stream 中的元素进行排序。

    1
    2
    3
    4
    List<String> list = Arrays.asList("banana", "apple", "cherry");
    list.stream()
    .sorted()
    .forEach(System.out::println); // 输出: apple, banana, cherry
  • distinct(): 去除 Stream 中的重复元素。

    1
    2
    3
    4
    List<String> list = Arrays.asList("apple", "banana", "apple");
    list.stream()
    .distinct()
    .forEach(System.out::println); // 输出: apple, banana
  • limit(long maxSize): 限制 Stream 中元素的数量。

    1
    2
    3
    4
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    list.stream()
    .limit(2)
    .forEach(System.out::println); // 输出: apple, banana
  • skip(long n): 跳过 Stream 中的前 n 个元素。

    1
    2
    3
    4
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    list.stream()
    .skip(1)
    .forEach(System.out::println); // 输出: banana, cherry
  • peek(Consumer): 对 Stream 中的每个元素执行操作,通常用于调试, 注意这个是中间操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

终端操作

终端操作会触发 Stream 的处理并返回结果,常见终端操作包括:

  • forEach(Consumer): 对 Stream 中的每个元素执行操作。

    1
    2
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    list.stream().forEach(System.out::println);
  • collect(Collector): 将 Stream 转换为集合或其他数据结构。

    1
    2
    3
    4
    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 元素。
    • 线程安全: 由于不可变性,该集合是天然线程安全的。
    • 性能特征: 创建速度快,但不支持高效的随机访问(如 ArrayListget(index))。
    • 与 List.of() 关系: 行为基本一致,都是返回不可变列表。
    1
    List<String> result = list.stream().toList();
  • reduce(T identity, BinaryOperator): 将 Stream 中的元素归约为一个值。

    1
    2
    3
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    int sum = numbers.stream()
    .reduce(0, Integer::sum); // 求和,结果为 10
  • count(): 返回 Stream 中元素的数量。

    1
    long count = list.stream().filter(s -> s.startsWith("a")).count(); // 统计以 "a" 开头的元素
  • anyMatch(Predicate) / allMatch(Predicate) / noneMatch(Predicate):

    • 检查 Stream 中是否存在/全部满足/全部不满足某个条件的元素。
    1
    boolean hasApple = list.stream().anyMatch(s -> s.equals("apple")); // true
  • findFirst() / findAny(): 查找 Stream 中的第一个元素或任意元素,返回 Optional。

    • findFirst(): 返回流中的第一个元素。在并行流中,为了保证“第一个”,性能开销较大。
    • findAny(): 返回流中的任意一个元素。在并行流中性能更好,因为它不需要维护顺序。
    1
    Optional<String> first = list.stream().findFirst(); // Optional[apple]

短路操作

某些操作具有“短路”特性,即一旦满足条件,Stream 就会停止处理:

  • findFirst()findAny(): 找到一个元素后立即结束。
  • anyMatch()allMatch()noneMatch(): 满足条件后立即返回结果。
  • limit(): 达到指定数量后停止处理。

数值流 (Primitive Streams)

Stream<T> 的泛型参数只能是引用类型,因此不存在 Stream<int>。把大量 intlongdouble 放进 Stream<Integer>Stream<Long>Stream<Double> 时,元素需要在基本类型与包装类型之间装箱、拆箱;这会增加对象数量、内存访问和 GC 压力。IntStreamLongStreamDoubleStream 分别直接传递三种基本类型,并提供对应的数值聚合操作。

它们不是“性能模式”开关:当数据量很小,或流水线本来就要创建领域对象时,差异通常不重要。数值计算、数组扫描和高频批处理则应尽量保持在数值流中,直到确实需要对象时才转换。

流类型元素类型常用专有终端操作空流的结果
IntStreamintsumaveragemax0OptionalDoubleOptionalInt
LongStreamlongsumaveragemax0LOptionalDoubleOptionalLong
DoubleStreamdoublesumaveragemax0.0OptionalDoubleOptionalDouble

创建数值流

of 适合已知的离散值;rangerangeClosed 适合整数区间。二者的起点都包含在结果中,差别只在终点:range 右开,rangeClosed 右闭。起点大于等于终点时,range 为空;起点大于终点时,rangeClosed 为空。

1
2
3
4
IntStream.of(3, 1, 4);             // 3, 1, 4
IntStream.range(1, 5); // 1, 2, 3, 4
IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
IntStream.empty(); // 空 IntStream

不要为了表达步长而把终点写错。range 不支持步长参数,应在流中转换或过滤;步长为 2 的偶数序列可这样写:

1
2
3
int[] evens = IntStream.range(0, 5)
.map(n -> n * 2)
.toArray(); // [0, 2, 4, 6, 8]

Java 9 起,iterate 提供带终止条件的重载;它适合按前一个值生成下一个值。generate 不依赖前一个元素,适合供应独立值。两者都可能是无限流,必须由条件或 limit 保证终端操作能结束。

1
2
3
4
5
6
IntStream.iterate(1, n -> n <= 16, n -> n * 2)
.forEach(System.out::println); // 1, 2, 4, 8, 16

double[] samples = DoubleStream.generate(Math::random)
.limit(3)
.toArray();

数组是数值流最常见的数据源。Arrays.stream(int[]) 直接得到 IntStream;传入数组片段时,fromIndex 包含、toIndex 不包含。Stream.of(int[]) 则会把整个数组当作一个 int[] 元素,不能替代 Arrays.stream(int[])

1
2
3
int[] values = {8, 3, 5, 2};
int total = Arrays.stream(values, 1, 4)
.sum(); // 3 + 5 + 2 = 10

在对象流与数值流之间转换

从对象流进入数值流时,使用 mapToIntmapToLongmapToDouble,映射函数直接返回基本类型。不要先 mapInteger 再求和,那会重新引入装箱。

1
2
3
4
5
6
7
8
9
10
List<String> words = List.of("java", "stream", "api");

int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // 13

double meanLength = words.stream()
.mapToInt(String::length)
.average()
.orElse(0.0);

数值流转为对象流有两种目的不同的 API:boxed() 生成对应的包装类型,而 mapToObj 可直接创建目标对象。只有后续 API 必须接收对象时才进行转换,并尽可能放在流水线末端。

1
2
3
4
5
6
7
8
List<Integer> squares = IntStream.rangeClosed(1, 4)
.map(n -> n * n)
.boxed()
.toList(); // [1, 4, 9, 16]

List<String> labels = IntStream.of(7, 9)
.mapToObj(n -> "item-" + n)
.toList(); // [item-7, item-9]

一对多映射应使用 flatMapToIntflatMapToLongflatMapToDouble。例如二维 int 数组的外层元素是 int[],必须先展平才能对所有数字求和。

1
2
3
4
int[][] matrix = {{1, 2}, {3, 4}};
int total = Arrays.stream(matrix)
.flatMapToInt(Arrays::stream)
.sum(); // 10

数值聚合与空流

数值流的 summinmaxaveragesummaryStatistics 避免了手写归约。minmaxaverage 没有可靠的空流默认值,因此返回 OptionalIntOptionalLongOptionalDoublesum 的数学单位元明确,所以空流返回 00L0.0

1
2
3
4
5
6
7
8
9
10
int[] scores = {70, 88, 92, 100};

IntSummaryStatistics stats = Arrays.stream(scores)
.filter(score -> score >= 80)
.summaryStatistics();

long count = stats.getCount(); // 3
int min = stats.getMin(); // 88
int max = stats.getMax(); // 100
double average = stats.getAverage(); // 280.0 / 3

summaryStatistics() 在一次终端遍历中同时得到计数、总和、最小值、最大值和平均值;需要多个指标时,比对同一条流分别调用多个终端操作更合适,因为 Stream 只能消费一次。

整数求和仍按目标类型计算。IntStream.sum() 的返回类型是 int,可能溢出;若 int 元素之和可能超过 int 范围,应在求和前提升:

1
2
3
long total = Arrays.stream(values)
.asLongStream()
.sum();

DoubleStream 使用二进制浮点数,常见小数相加会有舍入误差;货币等要求十进制精确计算的场景,应使用 BigDecimal,而不是 DoubleStream

数值流中的操作接口

数值流使用 IntPredicateIntUnaryOperatorIntConsumer 等专用函数式接口。调用方式与对象流相同,但参数和返回值保持基本类型:

1
2
3
4
int[] doubledOdds = IntStream.rangeClosed(1, 6)
.filter(n -> n % 2 != 0)
.map(n -> n * 2)
.toArray(); // [2, 6, 10]

三个数值流的 API 大体平行,但不要为了统一类型而过早转换。例如计数和索引使用 IntStream,文件大小和时间戳使用 LongStream,科学计算使用 DoubleStream;仅在跨类型计算时调用 asLongStreamasDoubleStream 或相应的 mapToXxx

无限流 (Infinite Streams)

Stream 可以是无限的,通常配合 limit() 使用:

  • iterate:

    1
    2
    3
    Stream.iterate(0, n -> n + 2)
    .limit(5)
    .forEach(System.out::println); // 0, 2, 4, 6, 8
  • generate:

    1
    2
    3
    Stream.generate(Math::random)
    .limit(3)
    .forEach(System.out::println);

数组与 Stream

数组本身没有 stream() 方法,需要借助 Arrays.stream()Stream.of() 创建流。两者在对象数组上等价,但在基本类型数组上行为差异很大。

创建数组流

  • 对象数组:

    1
    2
    3
    String[] fruits = {"apple", "banana", "cherry"};
    Stream<String> s1 = Arrays.stream(fruits); // 推荐写法
    Stream<String> s2 = Stream.of(fruits); // 与上等价
  • 基本类型数组: Arrays.stream()int[]long[]double[] 提供了专门重载,直接生成数值流,避免装箱开销:

    1
    2
    3
    4
    int[] numbers = {1, 2, 3, 4};
    IntStream intStream = Arrays.stream(numbers); // int[] -> IntStream
    LongStream longStream = Arrays.stream(new long[]{1L, 2L});
    DoubleStream doubleStream = Arrays.stream(new double[]{1.0, 2.0});
  • 截取数组的一部分:

    1
    2
    3
    int[] numbers = {1, 2, 3, 4, 5};
    Arrays.stream(numbers, 1, 4)
    .forEach(System.out::println); // 2, 3, 4(左闭右开)

Stream.of() 处理基本类型数组的陷阱

Stream.of(T...) 是可变参数方法。传入对象数组时,数组元素会被展开为流的元素;但传入基本类型数组时,由于 int[] 无法参与 T 的泛型推断,整个数组会被当成一个元素:

1
2
3
4
int[] numbers = {1, 2, 3, 4};

Stream.of(numbers).count(); // 1:整个 int[] 被当作一个元素
Arrays.stream(numbers).count(); // 4:正确展开为 IntStream

结论: 基本类型数组一律使用 Arrays.stream()

数组流的常见操作

  • 聚合计算:

    1
    2
    3
    4
    5
    6
    int[] numbers = {1, 2, 3, 4, 5, 6};
    int evenSum = Arrays.stream(numbers).filter(n -> n % 2 == 0).sum(); // 12
    OptionalInt max = Arrays.stream(numbers).max(); // OptionalInt[6]
    double avg = Arrays.stream(numbers).average().orElse(0.0); // 3.5
    IntSummaryStatistics stats = Arrays.stream(numbers).summaryStatistics();
    // stats 一次性提供 count、sum、min、max、average
  • 排序(不修改原数组):

    1
    2
    3
    int[] numbers = {3, 1, 2};
    int[] sorted = Arrays.stream(numbers).sorted().toArray(); // [1, 2, 3]
    // 注意与 Arrays.sort(numbers) 的区别:后者原地排序,会修改原数组
  • 对象数组与基本类型数组互转:

    1
    2
    3
    4
    5
    int[] numbers = {1, 2, 3};
    // int[] -> Integer[]:先 boxed 装箱为 Stream<Integer>
    Integer[] boxed = Arrays.stream(numbers).boxed().toArray(Integer[]::new);
    // Integer[] -> int[]:借助 mapToInt 拆箱
    int[] unboxed = Arrays.stream(boxed).mapToInt(Integer::intValue).toArray();

流转换回数组

  • 数值流: 直接调用无参 toArray():

    1
    2
    3
    int[] evens = Arrays.stream(new int[]{1, 2, 3, 4})
    .filter(n -> n % 2 == 0)
    .toArray(); // [2, 4]
  • 对象流: 使用带数组构造器引用的 toArray 重载:

    1
    String[] result = Stream.of("a", "b", "c").toArray(String[]::new);

    注意: 无参 toArray() 返回 Object[],直接强转为 String[] 会抛出 ClassCastException

二维数组

二维数组外层流的元素是子数组,使用 flatMap / flatMapToInt 展平:

1
2
3
4
5
int[][] matrix = {{1, 2}, {3, 4}};

int total = Arrays.stream(matrix) // Stream<int[]>
.flatMapToInt(Arrays::stream) // 展平为 IntStream: 1, 2, 3, 4
.sum(); // 10

数组与并行流

数组长度已知且内存连续,拆分成本极低,是并行流较合适的数据源之一:

1
long sum = Arrays.stream(largeArray).parallel().sum(); // 大数组求和可考虑并行

Stream 的特性

  • 惰性求值:

    • 中间操作不会立即执行,只有当终端操作触发时,Stream 才会从头到尾处理数据。这种机制优化了性能,避免了不必要的计算。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      list.stream()
      .filter(s -> {
      System.out.println("Filtering: " + s);
      return s.startsWith("a");
      })
      .forEach(System.out::println);
      // 只有调用 forEach 后,filter 才会执行
  • 不可变性:

    • Stream 操作不会修改原始数据源,结果通常是新的 Stream 或集合。
    • 示例:
      1
      2
      3
      List<String> list = Arrays.asList("apple", "banana");
      List<String> filtered = list.stream().filter(s -> s.startsWith("a")).toList();
      // list 保持不变,filtered 是新列表
  • 一次性使用:

    • 一个 Stream 只能被消费一次,终端操作后 Stream 会关闭。如果需要再次使用,必须重新创建 Stream。
    • 示例:
      1
      2
      3
      Stream<String> stream = list.stream();
      stream.forEach(System.out::println);
      // stream.forEach(System.out::println); // 抛出 IllegalStateException
  • 并行处理:

    • 通过 parallelStream()parallel(),Stream 可以并行处理数据,适合多核处理器环境。
    • 示例:
      1
      2
      3
      4
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
      numbers.parallelStream()
      .map(n -> n * 2)
      .forEach(System.out::println); // 顺序不保证

并行流并不是“自动加速开关”:拆分任务、协调线程和合并结果都有成本。下面的表格可用于初步判断是否值得考虑它。

场景是否考虑并行流原因
大批量、CPU 密集且每个元素计算独立可以评估有机会利用多核缩短计算时间
元素很少或单次处理极轻通常不建议并行调度成本可能高于收益
网络、磁盘、数据库等 I/O 操作不建议容易占用公共线程池,且瓶颈通常不在 CPU
依赖顺序、共享可变状态或复杂副作用谨慎使用需要额外同步或保序,容易引入错误

Collectors 的使用

Collectors 类提供了丰富的工具,用于将 Stream 的结果收集到集合或其他数据结构中。常见用法包括:

Collectors groupingBy:分类器与下游收集器的分工

这张图可以作为理解嵌套收集器的模板:groupingBy 的第一个参数只负责生成分组键,第二个参数才决定每一组中保存列表、计数、求和,还是继续做更深一层的分组。

  • 收集到集合:

    • Collectors.toList(): 底层默认使用 ArrayList
    • Collectors.toSet(): 底层默认使用 HashSet
    • Collectors.toMap(): 底层默认使用 HashMap
    1
    2
    3
    4
    5
    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,不建议强转
  • 分组:

    1
    2
    3
    Map<Integer, List<String>> byLength = list.stream()
    .collect(Collectors.groupingBy(String::length));
    // {5=[apple], 6=[banana, cherry]}
  • 分区:

    1
    2
    3
    Map<Boolean, List<String>> partitioned = list.stream()
    .collect(Collectors.partitioningBy(s -> s.length() > 5));
    // {false=[apple], true=[banana, cherry]}
  • 字符串拼接:

    1
    String joined = list.stream().collect(Collectors.joining(", ")); // apple, banana, cherry
  • 进阶收集器 (Downstream Collectors):
    groupingBy 支持嵌套收集器,实现多级分组或聚合:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 按长度分组后,统计每组的数量
    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())
    ));
  • 收集到指定集合类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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));

使用场景

Stream 适用于以下场景:

  • 数据过滤和转换: 从集合中筛选出符合条件的元素或进行格式转换。

    1
    2
    3
    4
    List<String> longNames = list.stream()
    .filter(s -> s.length() > 5)
    .map(String::toUpperCase)
    .toList();
  • 聚合计算: 求和、平均值、最大值、最小值等。

    1
    2
    3
    4
    5
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    double average = numbers.stream()
    .mapToInt(Integer::intValue)
    .average()
    .orElse(0.0);
  • 分组和统计: 对数据进行分组或统计分析。

    1
    2
    Map<Integer, Long> countByLength = list.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));
  • 并行处理: 处理大数据量时利用多核处理器。

    1
    2
    List<Integer> largeList = // 大数据量
    largeList.parallelStream().map(n -> heavyComputation(n)).toList();

注意事项

  1. 避免副作用:

    • Stream 操作应尽量避免修改外部状态(如修改共享变量),否则可能导致线程安全问题。
    • 错误示例:
      1
      2
      List<String> result = new ArrayList<>();
      list.stream().forEach(s -> result.add(s)); // 副作用,不推荐
      推荐:
      1
      List<String> result = list.stream().toList();
  2. 并行 Stream 的谨慎使用:

    • 并行 Stream 并不总是更快,适合 CPU 密集型任务(如计算)而非 I/O 密集型任务。
    • 并行处理可能导致顺序不一致(如 forEach 的输出顺序)。
    • 示例:
      1
      list.parallelStream().forEach(System.out::println); // 输出顺序不定
    • 线程池污染: 并行流默认使用 ForkJoinPool.commonPool()。如果在并行流中执行耗时的 I/O 操作,会阻塞该公共线程池,进而影响应用中其他使用该池的组件。
  3. 性能优化:

    • 使用合适的操作顺序以减少计算量。例如,先 filtermap 可以减少处理的数据量。
      1
      2
      3
      4
      list.stream()
      .filter(s -> s.length() > 5) // 先过滤
      .map(String::toUpperCase) // 再转换
      .toList();
  4. 异常处理:

    • Stream 操作中的异常需要显式处理,通常结合 try-catch 或 Optional。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      list.stream()
      .map(s -> {
      try {
      return someMethodThatMayThrow(s);
      } catch (Exception e) {
      return null;
      }
      })
      .toList();
  5. 避免在 Stream 中使用循环:

    • Stream 的设计初衷是取代显式循环,尽量使用内置操作(如 mapfilter)而非嵌套循环。
  6. 正确使用 peek():

    • peek() 主要用于调试,它是一个中间操作。如果没有终端操作,peek() 不会执行。
    • 不要用 peek() 来修改元素状态,这违反了函数式编程的无副作用原则。
  7. 不要依赖具体的集合实现进行强转:

    • 虽然 Collectors.toList()toSet()toMap() 目前在 OpenJDK 中分别返回 ArrayListHashSetHashMap,但规范并未保证这一点。
    • Java 16+ 的 Stream.toList() 返回的是不可变列表,强转为 ArrayList 会导致 ClassCastException
    • 如果明确需要特定的实现(如 ArrayListTreeSet 等),请显式使用 .collect(Collectors.toCollection(ArrayList::new)) 或在 toMap 中指定 supplier。

实际示例

以下是一个综合示例,展示 Stream 在处理复杂数据时的能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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; }
}

输出:

1
2
3
Count by age: {25=2, 30=1, 35=1}
Female names: [ALICE, DIANA]
Average age: 28.75

List 收集方式对比

在 Java 中有多种将流转换为列表的方法,它们的行为差异如下:

特性Collectors.toList()Collectors.toUnmodifiableList()Stream.toList() (Java 16+)
可变性可变 (通常是 ArrayList)不可变不可变
允许 Null否 (抛出 NPE)
强转 ArrayList可以 (但不推荐)
主要用途通用收集严格不可变且不含 null简洁、高效的不可变收集