欢迎光临
我们一直在努力

Java Stream流:从入门到精通

目录

引言

1. 什么是 Stream?

2. 创建 Stream

2.1 从集合创建

2.2 从数组创建

2.3 从值创建

2.4 从文件创建

2.5 无限流

2.6 基本类型流

3. 中间操作

3.1 filter

3.2 map

3.3 flatMap

3.4 distinct

3.5 sorted

3.6 peek

3.7 limit 和 skip

3.8 takeWhile 和 dropWhile

4. 收集器 Collectors 详解

4.1 收集到集合

4.2 收集到 Map

4.3 分组 groupingBy

4.4 分区 partitioningBy

4.5 字符串连接 joining

4.6 统计 summarizing

4.7 自定义收集器

5. 数值流

5.1 创建数值流

5.2 数值流操作

5.3 转换回对象流

6. 并行流

6.1 创建并行流

6.2 并行流注意事项

6.3 控制并行流使用的线程池

7. 常见陷阱与最佳实践

7.1 流只能消费一次

7.2 避免在 forEach 中修改外部状态

7.3 惰性求值的理解

7.4 使用 Optional 避免 NPE

7.5 合理使用并行流

7.6 优先使用基本类型流

7.7 不要过度使用 Stream

总结


引言

Java 8 的发布是 Java 语言演进史上的一个里程碑,其中最引人注目的特性之一就是 Stream API。Stream 提供了对集合数据高效、声明式处理的函数式编程范式,让开发者能够以接近自然语言的方式编写数据操作逻辑,同时利用多核架构并行执行,大大提升了代码的可读性和性能。

在传统的集合操作中,我们通常使用for循环或Iterator逐个处理元素,代码冗长且容易出错。而 Stream 将数据源(集合、数组等)抽象为流,通过一系列链式操作完成过滤、转换、聚合等复杂任务,代码简洁、易于维护。

本文将全面深入地介绍 Java Stream 的使用,从基础概念到高级特性,结合丰富的代码示例,帮助读者彻底掌握这一强大工具。

1. 什么是 Stream?

Stream(流)并不是一个数据结构,它不保存数据,而是对数据源(如集合、数组、I/O 资源)的一种视图或计算管道。Stream 的操作分为中间操作(返回一个新的 Stream)和终端操作(产生结果或副作用)。Stream 的特点:

  • 无存储:Stream 不是数据结构,只是数据源的一个映射。

  • 函数式编程:对 Stream 的操作会产生一个结果,但不会修改数据源。

  • 延迟执行:中间操作是惰性的,只有在终端操作执行时才会真正开始处理。

  • 可消费性:Stream 只能被消费一次,类似迭代器,用完后需要重新生成。

Stream 的工作流程可以概括为三个步骤:

  • 创建 Stream:从数据源获取一个流。

  • 中间操作:一个或多个中间操作组成处理管道。

  • 终端操作:执行管道并产生结果。


  • 2. 创建 Stream

    2.1 从集合创建

    这是最常用的方式,Collection接口提供了stream()和parallelStream()方法。

    List<String> list = Arrays.asList("a", "b", "c");
    Stream<String> stream = list.stream();

    2.2 从数组创建

    使用Arrays.stream()或Stream.of()。

    String[] array = {"a", "b", "c"};
    Stream<String> stream1 = Arrays.stream(array);
    Stream<String> stream2 = Stream.of(array);

    2.3 从值创建

    Stream<Integer> stream = Stream.of(1, 2, 3);

    2.4 从文件创建

    通过 Files.lines() 获取文件中的行流。

    try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.forEach(System.out::println);
    } catch (IOException e) {
    e.printStackTrace();
    }

    2.5 无限流

    使用 Stream.iterate()或Stream.generate() 创建无限流,通常需要配合 limit()使用。

    // 从0开始,每次+2,生成前10个偶数
    Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

    // 生成10个随机数
    Stream.generate(Math::random).limit(10).forEach(System.out::println);

    2.6 基本类型流

    为了避免装箱开销,提供了 IntStream、LongStream、DoubleStream。

    IntStream intStream = IntStream.range(1, 100); // [1, 99]
    IntStream intStream2 = IntStream.rangeClosed(1, 100); // [1, 100]

    3. 中间操作

    中间操作返回一个新的 Stream,可以链式调用。常见中间操作如下:

    3.1 filter

    过滤元素,保留满足条件的元素。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]

    3.2 map

    将元素转换成另一种形式。

    List<String> words = Arrays.asList("hello", "world");
    List<Integer> lengths = words.stream()
    .map(String::length)
    .collect(Collectors.toList()); // [5, 5]

    3.3 flatMap

    将每个元素转换成一个 Stream,然后将所有 Stream 连接成一个 Stream。常用于处理嵌套集合。

    List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
    );
    List<String> flatList = listOfLists.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList()); // [a, b, c, d]

    另一个例子:按单词拆分为字母。

    List<String> words = Arrays.asList("hello", "world");
    List<String> letters = words.stream()
    .flatMap(word -> Arrays.stream(word.split("")))
    .distinct()
    .collect(Collectors.toList());
    // [h, e, l, o, w, r, d]

    3.4 distinct

    去重,根据元素的 equals() 和 hashCode()。

    List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
    List<Integer> distinctNumbers = numbers.stream()
    .distinct()
    .collect(Collectors.toList()); // [1, 2, 3]

    3.5 sorted

    排序。

    List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
    List<Integer> sorted = numbers.stream()
    .sorted()
    .collect(Collectors.toList()); // [1, 1, 3, 4, 5, 9]

    // 自定义排序
    List<String> words = Arrays.asList("apple", "banana", "pear");
    List<String> sortedByLength = words.stream()
    .sorted(Comparator.comparingInt(String::length))
    .collect(Collectors.toList()); // [pear, apple, banana]

    3.6 peek

    对每个元素执行操作,通常用于调试。

    List<Integer> result = Stream.of(1, 2, 3, 4)
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("过滤后: " + n))
    .map(n -> n * n)
    .peek(n -> System.out.println("平方后: " + n))
    .collect(Collectors.toList());

    3.7 limit 和 skip

    • limit(n):截取前 n 个元素。

    • skip(n):跳过前 n 个元素。 常用于分页。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    // 第2页,每页3条
    List<Integer> page2 = numbers.stream()
    .skip(3) // 跳过第1页的3条
    .limit(3) // 取3条
    .collect(Collectors.toList()); // [4, 5, 6]

    3.8 takeWhile 和 dropWhile

    • takeWhile:从开头获取元素直到条件不成立。

    • dropWhile:从开头丢弃元素直到条件不成立。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    List<Integer> taken = numbers.stream()
    .takeWhile(n -> n < 4)
    .collect(Collectors.toList()); // [1, 2, 3]
    List<Integer> dropped = numbers.stream()
    .dropWhile(n -> n < 4)
    .collect(Collectors.toList()); // [4, 5, 6]

    4. 收集器 Collectors 详解

    java.util.stream.Collectors 提供了丰富的静态方法,用于常见的数据收集需求。

    4.1 收集到集合

    • toList():收集到 List。

    • toSet():收集到 Set。

    • toCollection(Supplier):收集到指定类型的集合。

    LinkedList<String> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));

    4.2 收集到 Map

    • toMap(Function keyMapper, Function valueMapper):需要处理键冲突(提供合并函数)。

    Map<Long, Shop> shopMap = shops.stream()
    .collect(Collectors.toMap(Shop::getId, Function.identity(), (oldVal, newVal) -> oldVal));

    4.3 分组 groupingBy

    根据分类函数对元素分组。

    // 按商铺类型分组
    Map<String, List<Shop>> shopsByType = shops.stream()
    .collect(Collectors.groupingBy(Shop::getType));

    // 下游收集器:统计每组数量
    Map<String, Long> countByType = shops.stream()
    .collect(Collectors.groupingBy(Shop::getType, Collectors.counting()));

    // 分组后映射
    Map<String, List<String>> namesByType = shops.stream()
    .collect(Collectors.groupingBy(Shop::getType, Collectors.mapping(Shop::getName, Collectors.toList())));

    4.4 分区 partitioningBy

    根据条件分成两组(true 和 false)。

    Map<Boolean, List<Shop>> highScoreShops = shops.stream()
    .collect(Collectors.partitioningBy(shop -> shop.getScore() > 4.5));

    4.5 字符串连接 joining

    String names = shops.stream()
    .map(Shop::getName)
    .collect(Collectors.joining(", "));

    4.6 统计 summarizing

    同时获取计数、总和、最小、最大、平均值。

    IntSummaryStatistics stats = shops.stream()
    .collect(Collectors.summarizingInt(Shop::getScore));
    System.out.println("平均分: " + stats.getAverage());

    4.7 自定义收集器

    可以通过实现 Collector 接口自定义收集逻辑,但多数场景使用 Collectors 已经足够。

    5. 数值流

    为了减少装箱开销,Stream API 提供了专门处理数值的流:IntStream、LongStream、DoubleStream。

    5.1 创建数值流

    • 从普通流转换:mapToInt(), mapToLong(), mapToDouble()

    • 使用 range 和 rangeClosed

    IntStream intStream = IntStream.rangeClosed(1, 100);

    5.2 数值流操作

    数值流提供了特有的终端操作,如 sum(), average(), min(), max() 等。

    int sum = shops.stream().mapToInt(Shop::getScore).sum();
    OptionalDouble avg = shops.stream().mapToDouble(Shop::getPrice).average();

    5.3 转换回对象流

    Stream<Integer> boxedStream = intStream.boxed();

    6. 并行流

    并行流利用 Fork/Join 框架将任务拆分成子任务并行执行,可以充分利用多核 CPU。

    6.1 创建并行流

    • 从集合获取:collection.parallelStream()

    • 将普通流转为并行流:stream.parallel()

    long count = list.parallelStream()
    .filter(s -> s.length() > 5)
    .count();

    6.2 并行流注意事项

    • 线程安全:确保流中的操作是线程安全的,避免共享可变状态。

    • 性能考量:数据量小或存在阻塞操作时,并行流可能更慢。

    • 有序性:并行流中 findAny 比 findFirst 更快。

    • 避免有状态操作:如 sorted、distinct 在并行流中开销较大。

    6.3 控制并行流使用的线程池

    默认使用 ForkJoinPool.commonPool(),可以通过设置系统属性调整线程数。但一般不建议自定义线程池,除非有特殊需求。

    7. 常见陷阱与最佳实践

    7.1 流只能消费一次

    Stream<String> stream = list.stream();
    stream.forEach(System.out::println);
    stream.forEach(System.out::println); // 抛出 IllegalStateException

    7.2 避免在 forEach 中修改外部状态

    应该使用 collect 或 reduce 进行无状态操作。

    7.3 惰性求值的理解

    中间操作不会立即执行,只有遇到终端操作才会触发计算。

    7.4 使用 Optional 避免 NPE

    findFirst 等操作返回 Optional,使用 ifPresent 或 orElse 处理空值。

    7.5 合理使用并行流

    数据量小或操作简单时,并行流可能得不偿失。建议在以下情况使用:

    • 数据量足够大(数万以上)

    • 每个元素处理耗时较长

    • 操作是无状态的,且易于拆分

    7.6 优先使用基本类型流

    对于大量数值操作,使用 IntStream 等可以避免装箱开销。

    7.7 不要过度使用 Stream

    简单的循环可能更易读,Stream 适用于复杂的数据转换和聚合。

    总结

    Stream API 是 Java 函数式编程的核心,它使得集合操作变得简洁而富有表现力。通过掌握 Stream 的创建、中间操作、终端操作以及并行流,开发者可以编写出更优雅、高效的代码。实际应用中,结合 Collectors 的丰富功能,几乎可以处理任何数据收集需求。

    希望本文能够帮助你全面理解和运用 Java Stream,在你的项目中充分发挥它的威力。如果你有任何疑问或想要深入探讨某个方面,欢迎在评论区留言交流!

    赞(0)
    未经允许不得转载:171主机测评 » Java Stream流:从入门到精通
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址