Java 8 Streams API:懒惰和性能优化


当你处理更大的数据或无限的流时,懒惰laziness是一个真正的福音, 处理数据时,我们不确定何时使用已处理的数据。eager急切的立即处理会以牺牲性能为代价,客户端可能只是使用一小部分数据。或者,根据某些条件,客户端甚至可能不需要利用该数据。
延迟处理应该基于“ 按需处理 ”策略。

目前的趋势是大数据的并行和实时处理,数据量不断增长且高性能两种要求带来挑战,Java Collections API模型可满足未来的这种跳转,Java 8 Streams API完全基于“ 仅按流程和按需处理 ”策略,因此必然支持懒惰laziness。 

在Java 8 Streams API中,管道中间的操作都是惰性的,并且其内部处理模型已经过优化,使其能够使用大量数据和高性能进行处理。让我们看一下它的例子:


//Created a Stream of a Students List
//attached a map operation on it
Stream < String > streamOfNames = students.stream()
    .map(student - > {
        System.out.println(
"In Map - " + student.getName());
        return student.getName();
    });
//Just to add some delay
for (int i = 1; i <= 5; i++) {
    Thread.sleep(1000);
    System.out.println(i +
" sec");
}
//Called a terminal operation on the stream
streamOfNames.collect(Collectors.toList());

输出:

1 sec
2 sec
3 sec
4 sec
5 sec
In Map - Tom
In Map - Chris
In Map - Dave

这里有一个在流上调用的Map操作,然后我们将延迟5秒,然后调用收集操作(终端操作)。为了证明懒惰,我们延迟了5秒。

性能优化:
如上所述,设计流的内部处理模型以优化处理流程。在处理流中,我们通常最终创建管道的各种中间操作和终端操作。各种中间操作通常会可能合并在一次性通过处理。

List < String > ids = students.stream()
    .filter(s - > {
        System.out.println("filter - " + s);
        return s.getAge() > 20;
    })
    .map(s - > {
        System.out.println(
"map - " + s);
        return s.getName();
    })
    .limit(3)
    .collect(Collectors.toList());


输出:

filter - 8
map - 8
filter - 9
map - 9
filter - 10
filter - 11
map - 11

上面的例子演示了这种行为,我们有两个中间操作,即map和filter。输出显示,它们都不会在可用流的整个大小上独立执行。
首先,id-8通过filter并立即移动到map。id-9的情况也是如此,而id-10没有通过filter检查。我们可以看到id-8,一旦通过过filter就立即可用于map操作,无论在filter之前还有多少元素仍然在流中排列。

短路方法:
Java 8 Streams API借助于短路操作优化了流处理。短路方法一旦满足条件就结束流处理。在通常的短路操作中,一旦满足条件,就会中断所有处于管道之前的中间操作。一些中间操作和终端操作具有此行为。
要查看它的工作原理,请尝试以下示例字符串名称列表:

//Somewhere down the line
//Just want two names from the steram
namesStream.limit(2).collect(Collectors.toList());

操作代码见下面:第一个流操作是(实际上没有意义)map,它以大写形式返回名称。第二个操作是filter,它只返回以“B”开头的名称。现在某个地方,如果我们通常调用它上面的Collect操作,map和filter是否看到处理列表中的所有名称?

//List of names
List < String > names = Arrays.asList(new String[] {
   
"barry",
   
"andy",
   
"ben",
   
"chris",
   
"bill"
});
//map and filter are piped and the stream is stored
Stream < String > namesStream = names.stream()
    .map(n - > {
        System.out.println(
"In map - " + n);
        return n.toUpperCase();
    })
    .filter(upperName - > {
        System.out.println(
"In filter - " + upperName);
        return upperName.startsWith(
"B");
    });

但是如果我们在Collect之前设置limit操作,则输出会发生显着变化。
输出:

In map - barry
In filter - BARRY
In map - andy
In filter - ANDY
In map - ben
In filter - BEN

我们可以清楚地看到limit(虽然它最近从其他地方调用,它是管道中的最后一个中间操作)对map和过滤操作有影响。整个管道说,我们想要前两个以字母“B”开头的名字。一旦管道处理以“B”开头的前两个名称,map和过滤器甚至不处理其余的名称。

现在,这可以证明是一个非常巨大的性能提升。考虑一下,如果我们的列表包含几千个名称,并且我们只想要匹配某个过滤条件的前几个名称,那么一旦我们得到预期的元素,就会跳过其余元素的处理。

anyMatch,allMatch,noneMatch,findFirst,findAny,limit和sub-stream等操作是Steams API中的这种短路方法。