Java Stream API 深度解析与实践指南

1. 概述

Java 8 引入了 Stream API,它为集合类提供了一种声明式的处理机制。Stream 不是数据结构,而是数据载体,提供了用于处理数据源(如:Collection、Arrays 或 I/O channels)的流式操作。它允许你以一种声明式的方式处理集合数据。Stream 就像是一个迭代器(Iterator),但它可以支持并行处理,并且提供了丰富的操作函数,使得对集合的操作更加简洁和高效。

Stream 的主要特点是:

  • 惰性求值: 大多数中间操作不会立即执行,只有当终端操作触发时才会开始处理。
  • 内部迭代: 与传统的 for 循环不同,Stream 使用内部迭代器,简化了代码逻辑。
  • 函数式编程: 支持 Lambda 表达式和方法引用,使代码更加简洁和易读。

2. 原理

Stream 的工作原理可以分为三个步骤:

  1. 创建 Stream: 从数据源(如集合、数组等)创建一个流。
  2. 中间操作: 对流进行一系列转换操作(如过滤、映射等),这些操作是惰性的,只会返回一个新的流对象。
  3. 终端操作: 触发流的执行,并产生结果或副作用。一旦终端操作执行完毕,流就会关闭且不能再次使用。

Stream 的操作分为中间操作和终端操作。中间操作会返回一个新的 Stream,它可以被链式调用,比如filter、map等;终端操作会触发 Stream 的处理,并返回一个结果或副作用,比如forEach、collect等。Stream 的处理是延迟的,只有在终端操作被调用时,中间操作才会真正执行,这种特性被称为 “惰性求值”。

3. Stream API 详解

3.1 创建 Stream

可以通过多种方式创建 Stream:

  1. 从集合创建:

Collection.stream() 和 Collection.parallelStream(): 从集合中创建流或并行流。

  List<String> list = Arrays.asList("apple", "banana", "orange");
  Stream<String> stream = list.stream();
  1. 从数组创建:
  String[] fruits = {"apple", "banana", "orange"};
  Stream<String> stream = Arrays.stream(fruits);
  1. 通过静态方法创建-Stream.of():
  Stream<String> stream = Stream.of("apple", "banana", "orange");

3.2 中间操作

中间操作不会立即执行,它们只是构建了一个流水线,直到遇到终端操作才会真正执行。

  1. filter(Predicate predicate)
  • 功能: 筛选符合条件的元素。
  • 实际应用场景:在一个用户列表中筛选出年龄大于 18 岁的用户。
  • 注意事项:filter操作是有状态的,大量数据时可能会占用较多内存。filter操作是延迟执行的,且不会改变原 Stream 的元素顺序。
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]
  1. map(Function mapper)
  • 功能: 将每个元素转换为另一种形式。
  • 实际应用场景:将一个商品列表中的商品价格打九折。
  • 注意事项:map操作不会改变原集合,返回的是新的 Stream,且是延迟执行的。
List<String> words = Arrays.asList("hello", "world");
List<Integer> lengths = words.stream()
                             .map(String::length)
                             .collect(Collectors.toList());
// 输出: [5, 5]
  1. flatMap(Function mapper)
  • 功能: 将多个流合并成一个流。
  • 实际应用场景:将一个包含多个子列表的列表扁平化。
  • 注意事项:flatMap操作后的 Stream 元素类型可能与原 Stream 不同。
List<List<Integer>> lists = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4)
);
List<Integer> flatList = lists.stream()
                              .flatMap(Collection::stream)
                              .collect(Collectors.toList());
// 输出: [1, 2, 3, 4]
  1. distinct()
  • 功能: 去除重复元素。通过元素的equals和hashCode方法判断元素是否重复。
  • 实际应用场景:在处理用户的操作日志时,可能会存在重复的操作记录,使用distinct可以快速去除重复记录,得到唯一的操作列表。
  • 注意事项:对于自定义对象的 Stream,如果要正确使用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]
  1. sorted()
  • 功能:对流中的元素进行自然排序(元素需实现Comparable接口),或根据指定的Comparator进行排序。
  • 实际应用场景:将一个学生成绩列表按成绩从高到低排序。
  • 注意事项:若元素未实现Comparable接口且未指定Comparator,会抛出ClassCastException。
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
List<Integer> sortedNumbers = numbers.stream()
                                     .sorted()
                                     .collect(Collectors.toList());
// 输出: [1, 1, 3, 4, 5, 9]
  1. peek(Consumer action)
  • 功能:对 Stream 中的每个元素执行一个操作,但不会改变元素本身,主要用于调试或在元素流经 Stream 时进行一些辅助操作,比如打印日志。
  • 实际应用场景:在数据处理流水线中,需要观察某个中间步骤的数据状态时,可以使用peek打印数据。比如在处理订单数据时,在计算订单总金额之前,查看每个订单的详细信息。
  • 注意事项:peek操作不应该用于有副作用的操作,因为在并行流中,其执行顺序是不确定的,可能会导致意想不到的结果。并且peek操作返回的还是原来的 Stream,不会改变 Stream 的元素结构。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .peek(System.out::println)
       .map(n -> n * 2)
       .collect(Collectors.toList());
// 输出: 1, 2, 3, 4, 5
  1. limit(n)
  • 功能:限制 Stream 的元素数量为前 n 个,返回包含前 n 个元素的新 Stream。
  • 实际应用场景:在分页查询中,获取第一页的数据,即限制返回数据的条数。
  • 注意事项:若 Stream 元素数量小于 n,则返回整个 Stream。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> limitedNumbers = numbers.stream()
                                     .limit(3)
                                     .collect(Collectors.toList());
  1. skip(long n)
  • 功能:跳过 Stream 中的前 n 个元素,返回一个不包含前 n 个元素的新 Stream。
  • 实际应用场景:常用于分页场景,比如获取第二页的数据,每页 10 条数据,就可以通过skip(10)跳过第一页的 10 条数据。另外,在处理大量日志数据时,如果已知前面部分数据是无效的,也可以用skip跳过。
  • 注意事项:如果 Stream 中元素数量小于 n,会返回一个空的 Stream。并且skip操作和limit操作经常一起使用来实现分页功能,使用时要注意两者的顺序和参数设置。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> skippedNumbers = numbers.stream()
                                     .skip(3)
                                     .collect(Collectors.toList());
// skippedNumbers 为 [4, 5, 6]

3.3 终端操作

终端操作会触发流的执行并产生结果或副作用。

  1. forEach(Consumer action)
  • 功能:对每个元素执行操作。
  • 实际应用场景:打印一个订单列表中的每个订单信息。
  • 注意事项:forEach是终端操作,不能再链式调用其他 Stream 操作。forEach是短路操作,若在并行流中使用,可能导致结果的不确定性。
List<String> words = Arrays.asList("hello", "world");
words.stream().forEach(System.out::println);
// 输出: hello, world
  1. collect(Collector collector)
  • 功能:将 Stream 中的元素收集到一个集合(如List、Set)或其他数据结构中,也可进行复杂的聚合操作。
  • 实际应用场景:将一个字符串列表中的单词去重后收集到一个 Set 中。
  • 注意事项:收集器的选择要根据实际需求,不同的收集器有不同的性能表现。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = numbers.stream()
                               .map(n -> n * 2)
                               .collect(Collectors.toList());
// 输出: [2, 4, 6, 8, 10]
  1. reduce(BinaryOperator accumulator)
  • 功能:对流中的元素进行累积操作。
  • 实际应用场景:计算一个订单列表中所有订单的总金额。
  • 注意事项:reduce操作可能会返回Optional类型,需要处理空值情况。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce(Integer::sum);
System.out.println(sum.orElse(0));
// 输出: 15
  1. count()
  • 功能:统计流中元素的数量。
  • 实际应用场景:统计一个班级中所有学生的人数,或者统计数据库查询结果集中记录的条数。
  • 注意事项:count是终端操作,执行后 Stream 就会被消耗,无法再次使用。如果 Stream 中的元素数量非常大,可能会对性能产生一定影响,此时可以考虑使用并行流优化,但要注意并行流带来的线程开销。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().count();
System.out.println(count);
// 输出: 5
  1. anyMatch(Predicate predicate)
  • 功能:判断是否至少有一个元素满足条件。
  • 实际应用场景:在一个权限列表中,判断是否有用户拥有某个特定权限,或者在商品库存列表中,判断是否有商品库存不足。
  • 注意事项:一旦找到匹配的元素,anyMatch就会立即返回true,不再继续处理 Stream 中的剩余元素。在并行流中,由于元素处理顺序不确定,可能会影响查找效率,所以在并行处理时要考虑数据特点和性能需求。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println(hasEven);
// 输出: true
  1. allMatch(Predicate predicate)
  • 功能:判断所有元素是否都满足条件。
  • 实际应用场景:检查一个班级中所有学生是否都通过了考试,或者验证一个订单列表中所有订单是否都已支付。
  • 注意事项:只要有一个元素不匹配条件,allMatch就会返回false,并停止处理剩余元素。在并行流中,需要注意元素处理顺序的不确定性可能对结果产生的影响。
List<Integer> numbers = Arrays.asList(2, 4, 6, 8);
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println(allEven);
// 输出: true
  1. noneMatch(Predicate predicate)
  • 功能:判断没有元素满足条件。与anyMatch逻辑相反。
  • 实际应用场景:检查一个文件目录中是否没有隐藏文件,或者在一个员工列表中判断是否没有员工的年龄超过退休年龄。
  • 注意事项:与anyMatch和allMatch类似,一旦确定结果就会停止处理。在并行处理时,要考虑由于处理顺序不确定可能带来的性能和结果准确性问题。
List<Integer> numbers = Arrays.asList(1, 3, 5, 7);
boolean noEven = numbers.stream().noneMatch(n -> n % 2 == 0);
System.out.println(noEven);
// 输出: true
  1. findFirst()
  • 功能:返回 Stream 中的第一个元素,如果 Stream 为空,返回Optional.empty()。
  • 实际应用场景:在一个按时间顺序排列的事件日志列表中,获取最早发生的事件,或者在一个排序后的商品价格列表中,获取价格最低的商品。
  • 注意事项:findFirst在并行流中表现不如findAny高效,因为它需要按顺序查找第一个元素。在并行流中,元素的顺序不确定,findFirst可能会因为需要等待第一个元素被处理而降低性能。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> first = numbers.stream().findFirst();
System.out.println(first.orElse(null));
// 输出: 1
  1. findAny()
  • 功能:返回任意一个元素(并行流时不确定)。如果 Stream 为空,返回Optional.empty()。
  • 实际应用场景:在一个用户列表中随机抽取一个用户进行奖励,或者在一组满足条件的商品中随机选择一个进行推荐。
  • 注意事项:在并行流中,findAny会比findFirst更高效,因为它不需要等待第一个元素,只要有元素可用就可以返回。但由于其返回的是任意元素,在对元素顺序有严格要求的场景下,需要谨慎使用。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> any = numbers.parallelStream().findAny();
System.out.println(any.orElse(null));
// 输出: 可能是任意一个元素

4. 实际应用场景

  1. 数据过滤与转换

假设我们有一个包含用户信息的列表,需要筛选出年龄大于 18 岁的用户,并将他们的名字转换为大写。

List<User> users = Arrays.asList(
    new User("Alice", 25),
    new User("Bob", 17),
    new User("Charlie", 30)
);

List<String> adultNames = users.stream()
                               .filter(user -> user.getAge() > 18)
                               .map(User::getName)
                               .map(String::toUpperCase)
                               .collect(Collectors.toList());

System.out.println(adultNames);
// 输出: [ALICE, CHARLIE]
  1. 文件处理

假设我们需要读取一个文件,统计其中每个单词出现的次数。

try (Stream<String> lines = Files.lines(Paths.get("input.txt"))) {
    Map<String, Long> wordCounts = lines.flatMap(line -> Arrays.stream(line.split("\\s+")))
                                        .filter(word -> !word.isEmpty())
                                        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

    System.out.println(wordCounts);
} catch (IOException e) {
    e.printStackTrace();
}

5. 注意事项

  1. 惰性求值: 中间操作不会立即执行,只有在终端操作时才会触发。因此,避免在中间操作中依赖外部状态。
  2. 不可重用: 一旦流被消费(即执行了终端操作),就不能再次使用。如果需要多次使用,可以重新创建流。
  3. 并行流: 虽然并行流可以提高性能,但并不是所有场景都适合使用。对于有序性和确定性要求较高的操作,建议使用串行流。
  4. 内存消耗: 某些操作(如 toList())可能会导致大量内存占用,尤其是在处理大数据集时。可以根据需求选择合适的收集器。
  5. 性能问题 :并行 Stream 在处理大数据集时可能会提高性能,但在小数据集或有复杂计算的情况下,并行处理可能会带来额外的开销。
  6. 状态共享 :在并行 Stream 中,避免使用共享可变状态,因为这可能会导致线程安全问题。
  7. 空指针处理 :在使用 Stream 操作时,要注意处理可能的空指针异常,特别是在map和filter等操作中。

你可能感兴趣的:(Java,数据结构,实战总结,java,算法,数据结构)