最近,我读了很多关于Java 8流的文章,特别是关于用Java 8流进行延迟加载的几篇文章:here和over here。我似乎无法撼动这种感觉,即惰性加载完全没有用(或者充其量只是语法上的便利,提供零性能值)。
让我们以以下代码为例:
int[] myInts = new int[]{1,2,3,5,8,13,21};
IntStream myIntStream = IntStream.of(myInts);
int[] myChangedArray = myIntStream
.peek(n -> System.out.println("About to square: " + n))
.map(n -> (int)Math.pow(n, 2))
.peek(n -> System.out.println("Done squaring, result: " + n))
.toArray();
这将登录控制台,因为调用了terminal operation
,在这种情况下为toArray()
,并且我们的流是惰性的,仅在调用终端操作时才执行。我当然也可以这样做:
IntStream myChangedInts = myIntStream
.peek(n -> System.out.println("About to square: " + n))
.map(n -> (int)Math.pow(n, 2))
.peek(n -> System.out.println("Done squaring, result: " + n));
什么也不会打印,因为地图没有发生,因为我不需要数据。直到我这样称呼:
int[] myChangedArray = myChangedInts.toArray();
瞧,我得到了映射的数据和控制台日志。除了我看到的任何零收益。我意识到我可以在调用toArray()
之前定义过滤器代码,并且可以绕过“未经过真正过滤的流”,但是那又是什么呢?受益吗?
这些文章似乎暗示与懒惰有关的性能提高,例如:
在Java 8 Streams API中,中间操作是惰性的,并且对其内部处理模型进行了优化,以使其能够以高性能处理大量数据。
和
Java 8 Streams API借助短路操作优化了流处理。短路方法在满足其条件后立即终止流处理。通常,短路操作是指一旦满足条件,就会中断流水线之前的所有中间操作。某些中间操作和终端操作都具有这种行为。
从字面上看,这听起来像是跳出了循环,完全没有惰性。
最后,第二篇文章中有这条困惑的线:
惰性操作可提高效率。这是一种不处理陈旧数据的方法。在逐渐消耗输入数据而不是事先拥有完整的元素集的情况下,惰性操作可能很有用。例如,考虑以下情况:使用Stream#generate(Supplier
)创建了无限流,并且提供的Supplier函数正在逐渐从远程服务器接收数据。在这种情况下,服务器调用只会在需要时在终端操作上进行。
无法处理过时的数据吗?什么?延迟加载如何使某人无法处理过时的数据?
TLDR:延迟加载除了能够在以后运行filter / map / reduce /无论执行什么操作(提供零性能优势)之外,还有什么好处?
如果是,那么实际的用例是什么?
答案 0 :(得分:15)
您的终端操作toArray()
可能支持您的论点,因为它需要流中的所有元素。
某些终端操作没有。对于这些,如果不懒惰地执行流,那将是浪费。两个例子:
//example 1: print first element of 1000 after transformations
IntStream.range(0, 1000)
.peek(System.out::println)
.mapToObj(String::valueOf)
.peek(System.out::println)
.findFirst()
.ifPresent(System.out::println);
//example 2: check if any value has an even key
boolean valid = records.
.map(this::heavyConversion)
.filter(this::checkWithWebService)
.mapToInt(Record::getKey)
.anyMatch(i -> i % 2 == 0)
第一个流将打印:
0
0
0
也就是说,中间操作将仅在一个元素上运行。这是一个重要的优化。如果不是很懒,那么所有peek()
调用都必须在所有元素上运行(绝对不必要,因为您只对一个元素感兴趣)。中间操作可能会很昂贵(例如在第二个示例中)
短路端子操作(不是toArray
)使这种优化成为可能。
答案 1 :(得分:5)
我有一个来自我们代码库的真实示例,因为我要简化它,所以不能完全确定您可能会喜欢它还是完全理解它...
我们有一个需要List<CustomService>
的服务,我想称之为。现在,为了调用它,我要去一个数据库(比现实要简单得多)并获得一个List<DBObject>
;为了从中获得List<CustomService>
,需要进行一些繁重的转换。
这是我的选择,进行原位转换并传递列表。很简单,但可能不是那么理想。第二种选择,重构服务,以接受List<DBObject>
和Function<DBObject, CustomService>
。这听起来很琐碎,但它使 laziness (以及其他功能)成为可能。该服务有时可能只需要该列表中的几个元素,或者有时是某个属性的max
等。因此,我不需要对所有元素进行繁重的转换,这Stream API
基于拉动的懒惰是赢家。
在Streams存在之前,我们曾经使用guava
。它的Lists.transform( list, function)
也很懒。
它不是流的基本特征,即使没有番石榴也可以完成,但是这样更简单。 findFirst
提供的示例很好,最容易理解;这是整个惰性的问题,仅在需要时才拉动元素,它们不会从中间操作大块地传递到另一个操作,而是一次从一个阶段传递到另一个阶段。
答案 2 :(得分:4)
您是对的,map().reduce()
或map().collect()
不会带来任何好处,但是findAny()
findFirst()
,{{1} },anyMatch()
等。基本上,任何可以短路的操作。
答案 3 :(得分:4)
惰性对您的API用户非常有用,尤其是在Stream
管道评估的最终结果可能非常大的情况下!
简单的示例是Java API本身中的Files.lines
方法。如果您不想将整个文件读入内存,而只需要前N
行,则只需编写:
Stream<String> stream = Files.lines(path); // lazy operation
List<String> result = stream.limit(N).collect(Collectors.toList()); // read and collect
答案 4 :(得分:1)
好问题。
假设您编写的是教科书上的完美代码,那么经过适当优化的for
和stream
之间的性能差异并不明显(在类加载方面,流通常会稍好一些,但差异不应如此)在大多数情况下都很明显)。
请考虑以下示例。
// Some lengthy computation
private static int doStuff(int i) {
try { Thread.sleep(1000); } catch (InterruptedException e) { }
return i;
}
public static OptionalInt findFirstGreaterThanStream(int value) {
return IntStream
.of(MY_INTS)
.map(Main::doStuff)
.filter(x -> x > value)
.findFirst();
}
public static OptionalInt findFirstGreaterThanFor(int value) {
for (int i = 0; i < MY_INTS.length; i++) {
int mapped = Main.doStuff(MY_INTS[i]);
if(mapped > value){
return OptionalInt.of(mapped);
}
}
return OptionalInt.empty();
}
鉴于上述方法,下一次测试应显示它们在大约相同的时间执行。
public static void main(String[] args) {
long begin;
long end;
begin = System.currentTimeMillis();
System.out.println(findFirstGreaterThanStream(5));
end = System.currentTimeMillis();
System.out.println(end-begin);
begin = System.currentTimeMillis();
System.out.println(findFirstGreaterThanFor(5));
end = System.currentTimeMillis();
System.out.println(end-begin);
}
OptionalInt [8]
5119
OptionalInt [8]
5001
无论如何,我们大部分时间都花在doStuff
方法中。假设我们要添加更多线程。
调整流方法很简单(考虑到您的操作满足并行流的先决条件)。
public static OptionalInt findFirstGreaterThanParallelStream(int value) {
return IntStream
.of(MY_INTS)
.parallel()
.map(Main::doStuff)
.filter(x -> x > value)
.findFirst();
}
在没有流的情况下实现相同的行为可能很棘手。
public static OptionalInt findFirstGreaterThanParallelFor(int value, Executor executor) {
AtomicInteger counter = new AtomicInteger(0);
CompletableFuture<OptionalInt> cf = CompletableFuture.supplyAsync(() -> {
while(counter.get() != MY_INTS.length-1);
return OptionalInt.empty();
});
for (int i = 0; i < MY_INTS.length; i++) {
final int current = MY_INTS[i];
executor.execute(() -> {
int mapped = Main.doStuff(current);
if(mapped > value){
cf.complete(OptionalInt.of(mapped));
} else {
counter.incrementAndGet();
}
});
}
try {
return cf.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
return OptionalInt.empty();
}
}
测试大约在同一时间再次执行。
public static void main(String[] args) {
long begin;
long end;
begin = System.currentTimeMillis();
System.out.println(findFirstGreaterThanParallelStream(5));
end = System.currentTimeMillis();
System.out.println(end-begin);
ExecutorService executor = Executors.newFixedThreadPool(10);
begin = System.currentTimeMillis();
System.out.println(findFirstGreaterThanParallelFor(5678, executor));
end = System.currentTimeMillis();
System.out.println(end-begin);
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
executor.shutdownNow();
}
OptionalInt [8]
1004
OptionalInt [8]
1004
最后,尽管我们不会从流中获得巨大的性能收益(考虑到您用for
替代方法编写了出色的多线程代码),但是代码本身往往会更易于维护。
最后的注释:
与编程语言一样,更高级别的抽象(相对于streams
而言,fors
)使开发人员更容易以性能为代价。我们没有从汇编语言,过程语言到面向对象语言,因为后者提供了更高的性能。我们之所以搬家是因为它提高了我们的生产力(以更低的成本开发相同的产品)。如果您能够像使用for
并正确编写多线程代码那样从流中获得相同的性能,那么我想这已经是一个胜利。
答案 5 :(得分:1)
一个未被提及的有趣用例是流中任意操作的组合,它们来自代码库的不同部分,可以响应不同种类的业务或技术要求。
例如,假设您有一个应用程序,其中某些用户可以看到所有数据,但某些其他用户只能看到其中的一部分。代码中检查用户权限的部分可以简单地对正在处理的流进行过滤。
没有惰性流,代码的同一部分可能会过滤已经实现的完整集合,但是获取它可能会很昂贵,而没有真正的收获。
或者,代码的同一部分可能希望将其过滤器附加到数据源,但是现在它必须知道数据是否来自数据库,因此它可以附加一个WHERE子句或其他一些源。
对于惰性流,它是可以以任何方式实现的过滤器。施加到数据库流上的过滤器可以转换为上述WHERE子句,与过滤整个表读取所导致的内存中集合相比,性能明显提高。
因此,更好的抽象,更好的性能,更好的代码可读性和可维护性听起来对我来说是一个胜利。 :)
答案 6 :(得分:0)
非惰性实现将处理所有输入,并将输出收集到每个操作的新集合中。显然,对于无限或足够大的源来说是不可能的,否则会消耗内存,并且在减少和短路操作的情况下不必要地消耗内存,因此有很大的好处。
答案 7 :(得分:0)
检查下面的例子
Stream.of("0","0","1","2","3","4")
.distinct()
.peek(a->System.out.println("after distinct: "+a))
.anyMatch("1"::equals);
如果它不是懒惰的行为您会期望所有元素首先通过 distinct
过滤。但是由于懒惰的执行,它的行为有所不同。它将流式传输计算结果所需的最少元素。
上面的例子将打印
after distinct: 0
after distinct: 1
分析工作原理:
第一个 "0"
一直到终端操作但不满足它。必须流式传输另一个元素。
第二个 "0"
通过 .distinct()
过滤,永远不会到达终端操作。
由于终端操作尚未满足,下一个元素被流式传输。
"1"
通过终端操作并满足。
不再需要流式传输元素。