Java函数式编程教程(五):Java Steam API

译:GentlemanTsao,2020-7-7


Java Stream API提供了一种处理对象集合的函数式方法。 Java Stream API是在Java 8中添加的,同时还具有其他一些函数式编程功能。 本Java Stream教程将解释这些函数式流的工作方式以及使用方法。

Java Stream API与Java IO的Java InputStream和Java OutputStream无关。 InputStream和OutputStream与字节流有关。 Java Stream API用于处理对象流,而不是字节流。

Java Steam 定义

Java Stream是一个能够对其元素进行内部迭代的组件,这意味着它可以对元素本身进行迭代。 相反,当您使用Java Collections迭代功能(例如,Java Iterator或使用Java Iterable的for-each循环)时,你必须自己实现元素的迭代。

Steam 处理

您可以将listener附加到Steam。 当Stream在内部迭代元素时,将调用这些listener。 Steam中的每个元素都将调用一次listener。 这样,每个listener都可以处理Steam中的每个元素。 这称为流式处理(stream processing)。

Steam的listener形成一条链。 链中的第一个listener可以处理流中的元素,然后为链中的下一个listener返回一个新元素以进行处理。 listener可以返回相同的元素,也可以返回新的元素,具体取决于该listener(处理器)的用途。

获取Steam

有很多方法来获取Java Steam,最常见方法是从Java集合获取。 如下是从Java List获取Steam的示例:

List<String> items = new ArrayList<String>();

items.add("one");
items.add("two");
items.add("three");

Stream<String> stream = items.stream();  

本示例首先创建一个Java列表,然后向其中添加三个字符串。 最后,调用stream()方法以获得Stream实例。

终结操作和中间操作

Stream接口可以选择终结和中间操作。 中间操作将listener添加到Stream而无需执行其他任何操作。 终结流操作启动元素的内部迭代,调用所有Stream并返回结果。

如下是一个Java Stream示例,其中包含一个中间操作和一个终结操作:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExamples {

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();

        stringList.add("ONE");
        stringList.add("TWO");
        stringList.add("THREE");

        Stream<String> stream = stringList.stream();

        long count = stream
            .map((value) -> { return value.toLowerCase(); })
            .count();

        System.out.println("count = " + count);

    }
}

Stream接口的map()方法是中间操作。 它仅在Stream上设置一个lambda表达式,将每个元素转换为小写。 map()方法将在后面详细介绍。

count()方法是终结操作。 此调用在内部启动迭代,将每个元素转换为小写字母然后进行计数。

元素转换到小写实际上并不影响元素的数量。 转换部分只是作为中间操作的示例。

中间操作

Java Stream API的中间流操作是对流中的元素进行转换或过滤的操作。 当向流添加中间操作时,将得到一个新的流。 新流是应用了中间操作的原始流产生的元素流。 如下是添加到流中的中间操作的示例,这会产生新的流:

List<String> stringList = new ArrayList<String>();

stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
    
Stream<String> stream = stringList.stream();
    
Stream<String> stringStream =
    stream.map((value) -> { return value.toLowerCase(); });

注意对Steam map()的调用。 该调用实际上返回一个新的Stream实例,该实例是已应用map操作的原始字符串流。

你只能将单个操作添加到给定的Stream实例。 如果需要将多个操作彼此链接在一起,则需要将第二个操作应用于第一个操作产生的Stream实例。 如下所示:

Stream<String> stringStream1 =
        stream.map((value) -> { return value.toLowerCase(); });

Stream<½String> stringStream2 =
        stringStream1.map((value) -> { return value.toUpperCase(); });

请注意,第二个Stream map()调用是在第一个map()调用返回的Stream上。

在Java Stream上链式调用中间操作是很常见的。 如下是在Java流上链式调用中间操作的示例:

Stream<String> stream1 = stream
  .map((value) -> { return value.toLowerCase(); })
  .map((value) -> { return value.toUpperCase(); })
  .map((value) -> { return value.substring(0,3); });

许多中间Stream操作可以将Java Lambda表达式作为参数。 该lambda表达式实现了适合给定中间操作的Java函数式接口。 例如,Function或Predicate接口。 中间操作方法参数的参数通常是函数式接口,这就是为什么它也可以由Java lambda表达式实现的原因。

filter()

Java Stream filter()可用于过滤Java Stream中的元素。 filter方法输入一个Predicate,该Predicate被Stream中的每个元素调用。 如果元素要包含在结果流中,则Predicate应返回true。 如果不应包含该元素,则Predicate应返回false。

如下是调用Java Stream filter()方法的示例:

Stream<String> longStringsStream = stream.filter((value) -> {
    return value.length() >= 3;
});

map()

Java Stream map()方法将一个元素转换(映射)到另一个对象。 例如,如果你有一个字符串列表,则可以将每个字符串转换为小写,大写或原始字符串的子字符串,或者完全转换成其他字符串。如下是一个Java Stream map()示例:

List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();

Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());

flatMap()

Java Stream flatMap()方法将单个元素映射到多个元素。 该理念是将每个元素从由多个内部元素组成的复杂结构“展平”到仅由这些内部元素组成的“展平”流。

例如,假设您有一个带有嵌套对象(子对象)的对象。 那么你可以将该对象映射到一个“平面”流,该流由自身加上其嵌套对象(或仅嵌套对象)组成。 你还可以将元素列表流映射到元素本身。 或将字符串流映射到这些字符串中的单词流,或映射到这些字符串中的各个Character实例。

如下是将字符串列表平面映射到每个字符串中的单词的示例。 这个例子展示了flatMap()如何将一个元素映射成多个元素。

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

stream.flatMap((value) -> {
    String[] split = value.split(" ");
    return (Stream<String>) Arrays.asList(split).stream();
})
.forEach((value) -> System.out.println(value))
;

此Java Stream flatMap()示例首先创建一个List,包含3个书名的字符串。 然后获取List的Stream,并调用flatMap()。

在Stream上调用的flatMap()操作必须返回另一个平坦映射元素的Stream。 在上面的示例中,每个原始字符串都被分解为多个单词,并变成一个列表,然后从该列表中获取并返回Steam。

请注意,此示例调用终结操作forEach()作为结束。 该调用仅在此处触发内部迭代,从而触发平面映射操作。 如果在Stream链上未调用任何终结操作,则不会发生任何事情。 实际上不会进行平面映射。

distinct()

Java Stream distinct()方法是一种中间操作,它返回一个新的Stream,仅包含与原始流不同的元素。 任何重复将被消除。 如下是Java Stream distinct()方法的示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();

List<String> distinctStrings = stream
        .distinct()
        .collect(Collectors.toList());

System.out.println(distinctStrings);

在此示例中,元素“one”在原始流中出现2次。由distinct()返回的Stream中仅包括第一次出现的元素。 因此,结果列表(通过调用collect())将仅包含one,two和three。 此示例打印的输出是:

[one, two, three]

limit()

Java Stream limit()方法可以将流中的元素数量限制为指定给limit()方法的数量。 limit()方法返回一个新的Stream,该Stream最多包含给定数量的元素。 如下是一个Java Stream limit()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();
stream
    .limit(2)
    .forEach( element -> { System.out.println(element); });  

本示例首先创建一个Stream,然后在其上调用limit(),然后使用带有lambda的forEach()来打印出该流中的元素。 由于调用了limit(2),仅将打印前两个元素。

peek()

Java Stream peek()方法是一种中间操作,它以Consumer(java.util.function.Consumer)作为参数,将为流中的每个元素调用Consumer。 peek()方法返回一个新的Stream,其中包含原始流中的所有元素。

顾名思义,peek()方法的目的是窥视Steam中的元素,而不是对其进行转换。 请记住,peek方法不会启动Steam中元素的内部迭代。 你还需要调用终结操作。 如下是一个Java Stream peek()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("abc");
stringList.add("def");

Stream<String> stream = stringList.stream();

Stream<String> streamPeeked = stream.peek((value) -> {
    System.out.println("value");
});

终结操作

Java Stream接口的终结操作通常返回单个值。 一旦在Stream上调用了终结操作,就将启动Stream的迭代以及链接流。 迭代完成后,将返回终结操作的结果。

终结操作通常不返回新的Stream实例。 因此,一旦在Stream上调用了终结操作,来自中间操作的Stream实例链就结束了。 这是在Java Stream上调用终结操作的示例:

long count = stream
  .map((value) -> { return value.toLowerCase(); })
  .map((value) -> { return value.toUpperCase(); })
  .map((value) -> { return value.substring(0,3); })
  .count();

该示例末尾的count()调用是终结操作。 由于count()返回long,因此中间操作的Stream链(map()调用)结束了。

anyMatch()

Java Stream anyMatch()方法是一个终结操作,它以单个Predicate作为参数,启动Stream的内部迭代,并将Predicate参数应用于每个元素。 如果Predicate对任何元素返回true,则anyMatch()方法返回true。 如果没有元素与Predicate匹配,则anyMatch()将返回false。 如下是一个Java Stream anyMatch()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

boolean anyMatch = stream.anyMatch((value) -> { return value.startsWith("One"); });
System.out.println(anyMatch);

在上面的示例中,anyMatch()方法将返回true,因为stream中的第一个字符串元素以“ One”开头。

allMatch()

Java Stream allMatch()方法是一个终端操作,它以单个Predicate作为参数,启动Stream中元素的内部迭代,并将Predicate参数应用于每个元素。 如果Predicate对于Stream中的所有元素都返回true,则allMatch()将返回true。 如果不是所有元素都与谓词匹配,则allMatch()方法将返回false。 如下是一个Java Stream allMatch()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

boolean allMatch = stream.allMatch((value) -> { return value.startsWith("One"); });
System.out.println(allMatch);

在上面的示例中,allMatch()方法将返回false,因为Stream中只有一个字符串以“ One”开头。

noneMatch()

Java Stream noneMatch()方法是一个终结操作,它将对Stream中的元素进行迭代并返回true或false,这取决于Stream中是否有元素与参数Predicate相匹配。 如果Predicate不匹配任何元素,则noneMatch()方法将返回true;如果匹配一个或多个元素,则方法将返回false。 如下是一个Java Stream noneMatch()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("abc");
stringList.add("def");

Stream<String> stream = stringList.stream();

boolean noneMatch = stream.noneMatch((element) -> {
    return "xyz".equals(element);
});

System.out.println("noneMatch = " + noneMatch);

collect()

Java Stream collect()方法是一个终结操作,它启动元素的内部迭代,并以集合或某种类型的对象收集流中的元素。 如下是一个简单的Java Stream collect()方法示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

List<String> stringsAsUppercaseList = stream
.map(value -> value.toUpperCase())
.collect(Collectors.toList());

System.out.println(stringsAsUppercaseList);

collect()方法采用Collectors(java.util.stream.Collector)作为参数。 实现Collectors需要对Collectors接口进行一些研究。 幸运的是,Java类java.util.stream.Collectors包含一组预先实现的Collectors,可以用于大多数常见操作的。 在上面的示例中,使用的是Collectors.toList()返回的Collector实现。 该Collectors只是将stream中的所有元素收集到标准Java List中。

count()

Java Stream count()方法是一个终结操作,用于启动Stream中元素的内部迭代并计算元素数量。如下是一个Java Stream count()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

long count = stream.flatMap((value) -> {
    String[] split = value.split(" ");
    return (Stream<String>) Arrays.asList(split).stream();
})
.count();

System.out.println("count = " + count);

此示例首先创建一个字符串列表,然后获取该列表的Stream,为其添加一个flatMap()操作,然后完成对count()的调用。 count()方法将启动Stream中元素的迭代,在flatMap()操作中将字符串元素拆分为单词,然后进行计数。 最终打印出来的结果是14。

findAny()

Java Stream findAny()方法可以从Stream中查找单个元素。 找到的元素可以来自Stream中的任何位置, 所以无法保证从流中何处获取元素。 如下是一个Java Stream findAny()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();

Optional<String> anyElement = stream.findAny();

System.out.println(anyElement.get());

注意findAny()方法返回Optional。 Stream可能为空,因此无法返回任何元素。可以使用Optional isPresent()方法检查是否找到了元素。

findFirst()

如果Stream中存在元素,则Java Stream findFirst()方法将找到Stream中的第一个元素。 findFirst()方法返回一个Optional,可以从中获取元素(如果存在)。 如下是一个Java Stream findFirst()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();

Optional<String> result = stream.findFirst();

System.out.println(result.get());

可以通过isPresent()方法检查Optional返回是否包含元素。

forEach()

Java Stream forEach()方法是一个终结操作,它启动Stream中元素的内部迭代,并将Consumer(java.util.function.Consumer)应用于Stream中的每个元素。 forEach()方法返回void。 如下是一个Java Stream forEach()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");

Stream<String> stream = stringList.stream();

stream.forEach( element -> { System.out.println(element); });

min()

Java Stream min()方法是一个终结操作,它返回Stream中的最小元素。 哪个元素最小是由传递给min()方法的Comparator实现确定的。 如下是一个Java Stream min()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("abc");
stringList.add("def");

Stream<String> stream = stringList.stream();

Optional<String> min = stream.min((val1, val2) -> {
    return val1.compareTo(val2);
});

String minString = min.get();

System.out.println(minString);

注意min()方法返回一个Optional,它可能包含也可能不包含结果。 如果Stream为空,则Optional get()方法将抛出NoSuchElementException。

max()

Java Stream max()方法是一个终结操作,它返回Stream中最大的元素。 哪个元素最大,取决于传递给max()方法的Comparator实现。 如下是一个Java Stream max()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("abc");
stringList.add("def");

Stream<String> stream = stringList.stream();

Optional<String> max = stream.max((val1, val2) -> {
    return val1.compareTo(val2);
});

String maxString = max.get();

System.out.println(maxString);

注意max()方法返回一个Optional,它可以包含也可以不包含结果。 如果Stream为空,则Optional get()方法将抛出NoSuchElementException。

reduce()

Java Stream reduce()方法是一个终结操作,可以将Stream中的所有元素缩减为单个元素。 如下是一个Java Stream reduce()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

Optional<String> reduced = stream.reduce((value, combinedValue) -> {
    return combinedValue + " + " + value;
});

System.out.println(reduced.get());

请注意reduce()方法返回的Optional。 此Optional包含传递给reduce()方法的lambda表达式返回的值(如果有)。 可以通过调用Optional get()方法获得该值。

toArray()

Java Stream toArray()方法是一个终结操作,它启动Stream中元素的内部迭代,并返回包含所有元素的Object数组。 如下是一个Java Stream toArray()示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream = stringList.stream();

Object[] objects = stream.toArray();

串联steam

Java Stream接口包含一个称为concat()的静态方法,该方法可以将两个Stream连接为一个。从而新的Stream包含第一个Stream中的所有元素和第二个Stream中的所有元素。 如下是使用Java Stream concat()方法的示例:

List<String> stringList = new ArrayList<String>();

stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");

Stream<String> stream1 = stringList.stream();

List<String> stringList2 = new ArrayList<>();
stringList2.add("Lord of the Rings");
stringList2.add("Planet of the Rats");
stringList2.add("Phantom Menace");

Stream<String> stream2 = stringList2.stream();

Stream<String> concatStream = Stream.concat(stream1, stream2);

List<String> stringsAsUppercaseList = concatStream
        .collect(Collectors.toList());

System.out.println(stringsAsUppercaseList);

从数组创建Steam

Java Stream接口包含一个称为of()的静态方法,该方法可用于从一个或多个对象创建Stream。 如下是使用Java Stream of()方法的示例:

Stream<String> streamOf = Stream.of("one", "two", "three");

评判Java Stream API

在使用过其他数据流API(例如Apache Kafka Streams API)之后,我想分享下对Java Stream API的评判。 这些不是严重的评判,但是当你尝试进行流处理时,有助于多一分警惕。

Steam是批处理,不是流式处理

尽管名为Steam,Java Stream API并不是真正的流处理API。 Java Stream API的终结操作返回该流中所有元素迭代的最终结果,并为这些元素提供中间和终结操作。 在处理完流中的最后一个元素之后,将返回终结操作的结果。

只有知道流中的最后一个元素是什么,才有可能在处理完流的最后一个元素之后返回最终结果。 知道给定元素是否是流中的最后一个元素的唯一方法是,你处理的是具有最后一个元素的批处理。 相反,真正的流没有最后一个元素。 你永远不知道给定的元素是否是最后一个。 因此,不可能对流执行终结操作。 最好的办法是在处理给定元素之后收集临时结果,但这属于采样,而不是最终结果。

Steam是链状,而非图状

Java Stream API的设计使Stream实例只能作用一次。 换句话说,只能将单个中间操作添加到Stream中,从而产生一个新的Stream对象。 你可以将另一个中间操作添加到生成的Stream对象,但不能添加到第一个Stream对象。 于是中间Stream实例的结构形成一条链。

在真正的流处理API中,根流(root stream)和事件侦听器(event listeners)通常可以形成图,而不仅仅是链。 多个侦听器可以侦听根流,并且每个侦听器都可以以自己的方式处理流中的元素,并因此可以回传转换后的元素作为结果。 因此,每个侦听器(中间操作)通常可以作为流本身供其他侦听器侦听其结果。 这就是Apache Kafka Streams的设计方式。 每个侦听器(中间流)也可以有多个侦听器。 最终的结构形成了一个带有监听器的监听器的监听器等的图。

相对于链,流处理图中没有单个最终操作。 最终操作是指可以保证是处理链中的最后一个操作。 相反,流处理图可以有多个最终操作。 图中的每个“叶子”都是最终操作。

当流处理结构可以是具有多个最终操作的图形时,流式API不能像Java Stream API那样轻松地支持终结操作。 为了轻松支持终结操作,必须有一个最终操作,从中返回最终结果。 基于图的流处理API可以支持“采样”操作,在该操作中,要求流处理图中的每个节点询问其内部保留的任何值(例如总和,如果有的话——纯转换侦听器节点不具有任何内部状态) 。

Steam是内部迭代,而非外部迭代

Java Stream API专门设计为具有Stream中元素的内部迭代。 在Stream上调用终结操作时,将开始迭代。 实际上,为了使终结操作能够返回结果,终结操作必须启动Stream中元素的迭代。

一些基于图的流处理API也同样设计为某种程度上向API用户隐藏元素的迭代(例如Apache Kafka Streams和RxJava)。 但是,个人而言,我更喜欢这样一种设计:每个流节点(根流和侦听器)可以通过方法将元素传递给它们,进而将该元素在整个图中传递并进行处理。 这样的设计将使测试图形中的每个侦听器变得更加容易,因为你可以配置图并在其后推送元素,最后检查结果(图的采样状态)。 这种设计还可以使流处理图可以通过图中的多个节点(而不仅仅是通过根流)推入元素到其中。

翻译花絮:

原文:
They aren’t big, important points of critique, but they are useful to have in the back of your head as you venture into stream processing.

译文:
这些不是严重的批判,但是当你尝试进行流式处理时,有助于让你多一分警惕。

猜你喜欢

转载自blog.csdn.net/GentelmanTsao/article/details/106939991