关于Supplier<Stream<T>> dataSrc
我想缓存Stream
项以进一步遍历相同的元素序列。在这种情况下,假设dataSrc
总是产生相同的序列(例如,获得温度为3摄氏度的Stream<Integer>
(参见下面的示例用法))。因此,选项1)首先收集 Stream
项,但是会浪费一次首次遍历将这些项添加到集合中:
Supplier<Stream<T>> dataSrc = ...
List<T> cache = dataSrc.collect(toList()); // **Additional** traversal to collect items
cache.stream().reduce(…) // 1st traversal
cache.stream().reduce(…) // 2nd traversal
... // Nth traversals
我想避免额外的遍历来收集项目和显式的cache
变量,并将其隐藏在Supplier<>
内,以便首次遍历项目隐式缓存,并在进一步遍历时从该缓存访问项目。我认为这类似于Reactor Project的反应流方法cache()的想法。
因此,我在下面的cache()
方法实现中概述了一个替代方案,尽管它已经存在两个问题(至少):1)onClose
在遍历完成时没有被调用(我不能弄清楚任何检测遍历结束的方法); 2)如果第一次遍历永不结束,则永远不会填充缓存。
Supplier<Stream<T>> dataSrc = cache(...)
dataSrc.get().reduce(…) // 1st traversal
dataSrc.get().reduce(…) // 2nd traversal
... // Nth traversals
static <T> Supplier<Stream<T>> cache(Supplier<Stream<T>> dataSrc) {
final List<T> cache = new ArrayList<>();
final AtomicBoolean started = new AtomicBoolean();
final AtomicBoolean isCached = new AtomicBoolean();
return () -> {
if(isCached.get()) return cache.stream();
if(!started.getAndSet(true)) {
return dataSrc
.get()
.peek(cache::add)
.onClose(() -> isCached.set(true));
}
return dataSrc.get();
};
}
是否有更好的方法来实现实用程序cache()
函数,该函数返回新的Stream<T>
缓存首次Stream
遍历的项目(没有首先收集隐式的额外遍历)并从该缓存创建更多Stream
个对象?
在这里,我从World Weather online API获得了3月份的温度。要执行它,您必须在给定的URI中包含AsyncHttpClient的依赖关系和有效的API密钥。
Pattern pat = Pattern.compile("\\n");
boolean [] isEven = {true};
CompletableFuture<Stream<Integer>> temps = asyncHttpClient()
.prepareGet("http://api.worldweatheronline.com/premium/v1/past-weather.ashx?q=37.017,-7.933&date=2018-03-01&enddate=2018-03-31&tp=24&format=csv&key=715b185b36034a4c879141841182802")
.execute()
.toCompletableFuture()
.thenApply(Response::getResponseBody)
.thenApply(pat::splitAsStream)
.thenApply(str -> str
.filter(w -> !w.startsWith("#")) // Filter comments
.skip(1) // Skip line: Not Available
.filter(l -> isEven[0] = !isEven[0]) // Filter Even line
.map(line -> line.substring(14, 16)) // Extract temperature in celcius
.map(Integer::parseInt)
);
请注意,CompletableFuture<Stream<Integer>>
在功能上符合Supplier<Stream<Integer>>
。虽然CompletableFuture
缓存了生成的流,但它不能迭代两次。
问题1 :以下代码抛出IllegalStateException: stream has already been operated upon or closed
out.println(temps.join().distinct().count());
out.println(temps.join().max(Integer::compare)); // throws IllegalStateException
问题2 :在List
中收集它会导致第一次遍历,因此我们将有3次遍历,而不是2次:
CompletableFuture<List<Integer>> list = temps.thenApply(str -> str.collect(toList()));
out.println(list.join().stream().distinct().count()); // 2 traversals
out.println(list.join().stream().distinct().max(Integer::compare));// 1 traversal
目标:首次遍历时将项目存储在缓存中。每次流检索项目时,都应将其存储在将用于进一步遍历的内部缓存中。
Supplier<Stream<Integer>> cache = Cache.of(temps::join);
out.println(temps.get().distinct().count()); // 1 traversal
out.println(temps.get().max(Integer::compare)); // 1 traversal form cache
答案 0 :(得分:2)
我认为检测Stream
遍历结束的唯一方法是通过iterator()
或spliterator()
。因此,获得可重放Stream
的更好选择是从其迭代器记录其项目(由示例的Recorder
类完成),然后实现一个新的Spliterator
来读取先前记录的项目(由cacheIterator()
完成)。在此解决方案中,我使用getOrAdvance()
synchronized
Recorder
方法来保证只有一个结果流将从源中获取新项目。
因此,Cache.of(dataSrc)
创建了一个链:
dataSrc
---->
Recorder
---->
cacheIterator()
---->
Stream
旁注:
Cache.of()
得到的流允许有限的并行性。为了获得更好的拆分支持,cacheIterator()
下面应该返回Spliterator
实现,例如AbstractList.RandomAccessSpliterator
。Recorder
/ cacheIterator()
解决方案也适用于可以在以后短路的无限数据源。 E.g。它可以缓存无限流nrs
的项目,并在没有缓存(或nrsReplay
)的情况下打印输出:
Random rnd = new Random();
Supplier<Stream<String>> nrs = () -> Stream.generate(() -> rnd.nextInt(99)).map(Object::toString);
IntStream.range(1, 6).forEach(size -> out.println(nrs.get().limit(size).collect(joining(","))));
System.out.println();
Supplier<Stream<String>> nrsReplay = Cache.of(nrs);
IntStream.range(1, 6).forEach(size -> out.println(nrsReplay.get().limit(size).collect(joining(","))));
输出:
32
65,94
94,19,34
72,77,66,18
88,41,34,97,2893
93,65
93,65,71
93,65,71,40
93,65,71,40,68
class Cache {
public static <T> Supplier<Stream<T>> of(Supplier<Stream<T>> dataSrc) {
final Spliterator<T> src = dataSrc.get().spliterator(); // !!!maybe it should be lazy and memorized!!!
final Recorder<T> rec = new Recorder<>(src);
return () -> {
// CacheIterator starts on index 0 and reads data from src or
// from an internal cache of Recorder.
Spliterator<T> iter = rec.cacheIterator();
return StreamSupport.stream(iter, false);
};
}
static class Recorder<T> {
final Spliterator<T> src;
final List<T> cache = new ArrayList<>();
final long estimateSize;
boolean hasNext = true;
public Recorder(Spliterator<T> src) {
this.src = src;
this.estimateSize = src.estimateSize();
}
public synchronized boolean getOrAdvance(
final int index,
Consumer<? super T> cons) {
if (index < cache.size()) {
// If it is in cache then just get if from the corresponding index.
cons.accept(cache.get(index));
return true;
} else if (hasNext)
// If not in cache then advance the src iterator
hasNext = src.tryAdvance(item -> {
cache.add(item);
cons.accept(item);
});
return hasNext;
}
public Spliterator<T> cacheIterator() {
return new Spliterators.AbstractSpliterator<T>(
estimateSize, src.characteristics()
) {
int index = 0;
public boolean tryAdvance(Consumer<? super T> cons) {
return getOrAdvance(index++, cons);
}
public Comparator<? super T> getComparator() {
return src.getComparator();
}
};
}
}
}
答案 1 :(得分:1)
您可以使用Guava的Suppliers#memoize功能将给定的供应商转变为缓存(&#34; memoizing&#34;)。
Supplier<Stream<T>>
变为收集信息流的Supplier<List<T>>
这将是您的cache()
方法:
private static <T> Supplier<Stream<T>> cache(Supplier<Stream<T>> dataSrc) {
Supplier<List<T>> memoized = Suppliers.memoize(() -> dataSrc.get().collect(toList()));
return () -> memoized.get().stream();
}
(在Guava中混音时,您可能需要在Guava的c.g.c.b.Supplier版本和java.util.Supplier之间切换,并且可以轻松地来回转换它们,但在这种情况下它&# 39;甚至没有必要)
示例强>
假设一个简单的Integer流返回前5个自然数并将计算报告给stdout:
private static Supplier<Stream<Integer>> getDataSrc() {
return () -> IntStream.generate(new IntSupplier() {
private int i = 0;
@Override
public int getAsInt() {
System.out.println("Computing next i: " + (i + 1));
return i += 1;
}
}).limit(5).boxed();
}
然后运行非记忆版
Supplier<Stream<Integer>> dataSrc = getDataSrc();
System.out.println(dataSrc.get().collect(toList()));
System.out.println(dataSrc.get().collect(toList()));
产量
计算下一个i:1
计算下一个i:2
计算下一个i:3
计算下一个i:4
计算下一个i:5
[1,2,3,4,5]
计算下一个i:1
计算下一个i:2
计算下一个i:3
计算下一个i:4
计算下一个i:5
[1,2,3,4,5]
运行memoized版本
Supplier<Stream<Integer>> dataSrc = cached(getDataSrc());
System.out.println(dataSrc.get().collect(toList()));
System.out.println(dataSrc.get().collect(toList()));
产量
计算下一个i:1
计算下一个i:2
计算下一个i:3
计算下一个i:4
计算下一个i:5
[1,2,3,4,5]
[1,2,3,4,5]
答案 2 :(得分:1)
如果使用Reactor Project是一个选项,那么您只需将Supplier<Stream<T>>
转换为Flux<T>
,cache()
已提供实用程序Flux<T>
方法,然后使用{{} 1}}操作而不是Stream<T>
操作。
关于原始帖子的示例,其中temps
是CompletableFuture<Stream<Integer>>
,其中HTTP请求的结果是以摄氏度的温度序列转换的,那么我们可以通过以下方式执行这两个查询:
Flux<Integer> cache = Flux.fromStream(temps::join).cache();
cache.distinct().count().subscribe(out::println);
cache.reduce(Integer::max).subscribe(out::println);
此解决方案避免:1)IllegalStateException
进一步遍历此序列; 2)第一次遍历以收集缓存中的项目。