译:GentlemanTsao,2020-7-7
Java Stream API与Java IO的Java InputStream和Java OutputStream无关。 InputStream和OutputStream与字节流有关。 Java Stream API用于处理对象流,而不是字节流。
Java Stream是一个能够对其元素进行内部迭代的组件,这意味着它可以对元素本身进行迭代。 相反,当您使用Java Collections迭代功能(例如,Java Iterator或使用Java Iterable的for-each循环)时,你必须自己实现元素的迭代。
您可以将listener附加到Steam。 当Stream在内部迭代元素时,将调用这些listener。 Steam中的每个元素都将调用一次listener。 这样,每个listener都可以处理Steam中的每个元素。 这称为流式处理(stream processing)。
Steam的listener形成一条链。 链中的第一个listener可以处理流中的元素,然后为链中的下一个listener返回一个新元素以进行处理。 listener可以返回相同的元素,也可以返回新的元素,具体取决于该listener(处理器)的用途。
有很多方法来获取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表达式实现的原因。
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;
});
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());
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链上未调用任何终结操作,则不会发生任何事情。 实际上不会进行平面映射。
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]
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),仅将打印前两个元素。
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()调用)结束了。
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”开头。
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”开头。
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);
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中。
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。
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()方法检查是否找到了元素。
如果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返回是否包含元素。
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); });
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。
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。
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()方法获得该值。
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();
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);
Java Stream接口包含一个称为of()的静态方法,该方法可用于从一个或多个对象创建Stream。 如下是使用Java Stream of()方法的示例:
Stream<String> streamOf = Stream.of("one", "two", "three");
在使用过其他数据流API(例如Apache Kafka Streams API)之后,我想分享下对Java Stream API的评判。 这些不是严重的评判,但是当你尝试进行流处理时,有助于多一分警惕。
尽管名为Steam,Java Stream API并不是真正的流处理API。 Java Stream API的终结操作返回该流中所有元素迭代的最终结果,并为这些元素提供中间和终结操作。 在处理完流中的最后一个元素之后,将返回终结操作的结果。
只有知道流中的最后一个元素是什么,才有可能在处理完流的最后一个元素之后返回最终结果。 知道给定元素是否是流中的最后一个元素的唯一方法是,你处理的是具有最后一个元素的批处理。 相反,真正的流没有最后一个元素。 你永远不知道给定的元素是否是最后一个。 因此,不可能对流执行终结操作。 最好的办法是在处理给定元素之后收集临时结果,但这属于采样,而不是最终结果。
Java Stream API的设计使Stream实例只能作用一次。 换句话说,只能将单个中间操作添加到Stream中,从而产生一个新的Stream对象。 你可以将另一个中间操作添加到生成的Stream对象,但不能添加到第一个Stream对象。 于是中间Stream实例的结构形成一条链。
在真正的流处理API中,根流(root stream)和事件侦听器(event listeners)通常可以形成图,而不仅仅是链。 多个侦听器可以侦听根流,并且每个侦听器都可以以自己的方式处理流中的元素,并因此可以回传转换后的元素作为结果。 因此,每个侦听器(中间操作)通常可以作为流本身供其他侦听器侦听其结果。 这就是Apache Kafka Streams的设计方式。 每个侦听器(中间流)也可以有多个侦听器。 最终的结构形成了一个带有监听器的监听器的监听器等的图。
相对于链,流处理图中没有单个最终操作。 最终操作是指可以保证是处理链中的最后一个操作。 相反,流处理图可以有多个最终操作。 图中的每个“叶子”都是最终操作。
当流处理结构可以是具有多个最终操作的图形时,流式API不能像Java Stream API那样轻松地支持终结操作。 为了轻松支持终结操作,必须有一个最终操作,从中返回最终结果。 基于图的流处理API可以支持“采样”操作,在该操作中,要求流处理图中的每个节点询问其内部保留的任何值(例如总和,如果有的话——纯转换侦听器节点不具有任何内部状态) 。
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.
译文:
这些不是严重的批判,但是当你尝试进行流式处理时,有助于让你多一分警惕。