Java Stream API

“【曾续缘聊技术】专栏持续更新中!我是深耕计算机领域的曾续缘,专注用通俗语言讲透硬核知识。关注+星标,获取最新技术干货推送!”

Java Stream API 是 Java 8 中引入的一个强大特性,它提供了一种高效且易于理解的数据处理方式。在处理集合时,Stream API 允许我们以声明式的方式表达复杂的数据处理操作,从而简化代码并提高效率。

Java Stream API基础

创建Stream

  • 通过集合:任何集合都可以通过调用 stream() 方法来创建一个流。
    List<String> list = Arrays.asList("a", "b", "c");
    Stream<String> stream = list.stream();
    
  • 通过数组:使用 Arrays.stream(Object[]) 方法。
    int[] array = {1, 2, 3};
    IntStream stream = Arrays.stream(array);
    
  • 直接创建:使用 Stream.of(T... values) 方法。
    Stream<String> stream = Stream.of("a", "b", "c");
    

操作分类

中间操作

中间操作(Intermediate Operations)返回的是一个新的流,因此可以将其链接起来,形成一个操作链。这些操作不会立即执行,只有当终端操作被调用时,整个操作链才会开始执行。

例如,下面的代码链包含了两个中间操作mapfilter,它们将转换元素为大写,并过滤出以"A"开头的字符串:

Stream<String> stream = list.stream()
                             .map(String::toUpperCase)
                             .filter(s -> s.startsWith("A"));

在这个例子中,直到调用终端操作之前,中间操作不会执行任何操作。

中间操作分为无状态操作和有状态操作,这两类操作在处理元素时的行为和性能特征有所不同。

无状态操作

无状态操作(Stateless Operations)不维护任何状态信息,它们处理每个元素时都是独立的,不需要考虑其他元素的状态。这意味着这些操作可以并行执行,而不会影响结果。常见的无状态操作包括:

  • map:将每个元素转换成另一个形式。
  • flatMap:将每个元素转换成流,然后将这些流扁平化成一个流。
  • filter:过滤出满足特定条件的元素。
  • peek:执行一个无副作用的操作,通常用于调试。

例如,map操作将每个元素应用一个函数,而不会影响其他元素:

Stream<String> upperCase = list.stream().map(String::toUpperCase);

在这个例子中,每个元素都被独立地转换为大写,这个操作不需要知道其他元素的状态。

有状态操作

有状态操作(Stateful Operations)在处理元素时会维护一个或多个状态,这些状态可能会影响其他元素的处理。这意味着这些操作通常不能并行执行,或者需要特殊的处理来保证并行执行的正确性。常见的有状态操作包括:

  • sorted:对元素进行排序。
  • distinct:去除重复元素。
  • limit:限制流的大小。
  • skip:跳过流中的前几个元素。

例如,sorted操作会对流中的所有元素进行排序,这需要知道所有元素的比较关系:

Stream<String> sorted = list.stream().sorted();

在这个例子中,为了对元素进行排序,sorted操作需要收集所有元素并比较它们,这是一个有状态的操作。

终端操作

终端操作(Terminal Operations)会触发流的处理,并返回一个结果或产生副作用。终端操作会遍历流中的元素,并对它们应用中间操作链中定义的处理。终端操作执行后,流就被消耗掉了,不能再次使用。终端操作包括以下几种类型:

  • 遍历操作:如forEach,用于遍历每个元素并执行一个操作。
  • 收集操作:如collect,用于将流中的元素收集到一个集合中。
  • 规约操作:如reduce,用于将流中的所有元素规约为一个结果。
  • 查找操作:如findFirstfindAny,用于查找流中的元素。
  • 匹配操作:如anyMatchallMatchnoneMatch,用于检查流中的元素是否满足某个条件。

例如,下面的代码使用了forEach作为终端操作,它会遍历流中的每个元素,并打印出来:

stream.forEach(System.out::println);

在这个例子中,forEach操作会立即执行,并且流中的元素会按照中间操作链定义的方式被处理和打印。

深入理解Stream操作

操作原理

流水线(Pipelining)

Java Stream的流水线设计允许将多个操作链接起来,形成一个操作链。每个操作都返回一个新的流,这样可以方便地将多个操作组合起来,而不需要创建临时集合。流水线使得Stream API易于使用且表达性强。

例如,下面的代码将多个中间操作和终端操作链接起来,形成一个流水线:

list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

在这个例子中,filtermapsortedforEach操作被链接起来,形成了一个操作链。

惰性求值(Lazy Evaluation)

Java Stream的中间操作是惰性求值的,这意味着它们不会立即执行。相反,它们会创建一个描述操作的流水线,只有当终端操作被调用时,这个流水线才会被实际执行。这种设计允许对整个操作链进行优化。

例如,下面的代码只创建了操作链,并没有执行任何操作:

Stream<String> stream = list.stream()
                             .filter(s -> s.startsWith("A"))
                             .map(String::toUpperCase);

直到调用终端操作,比如forEach,上面的操作链才会被执行:

stream.forEach(System.out::println);

并行流(Parallel Streams)

并行流可以将顺序流转换为并行流,从而可能提高某些操作的执行效率。并行流利用多核处理器的并发能力,通过将任务分散到多个线程上执行,来加速数据处理。

使用并行流

使用parallel()方法可以将顺序流转换为并行流:

Stream<String> parallelStream = list.stream().parallel();

注意事项

  • 性能考量:并非所有操作都适合并行化。对于数据量较小或者操作本身开销不大的情况,并行流可能不会带来性能提升,反而可能因为线程切换和同步的开销而降低性能。
  • 线程安全:在使用并行流时,需要确保操作是线程安全的。如果流操作涉及到共享状态的修改,可能需要额外的同步措施。
  • 有序性:并行流可能会改变操作的顺序性。如果操作需要保持元素的顺序,应该使用顺序流或者确保并行流中的操作是顺序无关的。

高级应用

在Java Stream API的高级应用中,我们会遇到一些更复杂的数据处理场景,需要使用flatMapreduce等操作来处理。同时,为了提高性能,我们需要遵循一些最佳实践,比如避免在中间操作中修改共享变量,并选择合适的流类型(并行流或顺序流)。

复杂数据处理

使用 flatMap 进行扁平化操作

flatMap方法用于将多个流合并成一个流。当你有一个包含多个集合的流,并且想要将这些集合中的元素合并到一个流中时,flatMap非常有用。

例如,假设我们有一个单词列表,每个单词都是一个字符串流,我们想要将这些单词的字符扁平化到一个流中:

List<String> words = Arrays.asList("hello", "world");
Stream<String> letters = words.stream()
                              .flatMap(word -> Stream.of(word.split("")));

使用 reduce 进行累加操作

reduce方法用于将流中的所有元素合并成一个结果。这通常用于求和、求乘积或其他类型的累加操作。

例如,计算一个整数列表的所有元素的和:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);

参考链接:https://cengxuyuan.cn

你可能感兴趣的:(java,大数据)