如何缓存和重播供应商的项目<stream <t>&gt;

时间:2018-04-17 11:35:02

标签: java java-8 java-stream

关于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

3 个答案:

答案 0 :(得分:2)

我认为检测Stream遍历结束的唯一方法是通过iterator()spliterator()。因此,获得可重放Stream的更好选择是从其迭代器记录其项目(由示例的Recorder类完成),然后实现一个新的Spliterator来读取先前记录的项目(由cacheIterator()完成)。在此解决方案中,我使用getOrAdvance() synchronized Recorder方法来保证只有一个结果流将从源中获取新项目。

因此,Cache.of(dataSrc)创建了一个链:

dataSrc ----> Recorder ----> cacheIterator() ----> Stream

旁注

  1. 方法Cache.of()得到的流允许有限的并行性。为了获得更好的拆分支持,cacheIterator()下面应该返回Spliterator实现,例如AbstractList.RandomAccessSpliterator
  2. 虽然这不是必需的,但Recorder / cacheIterator()解决方案也适用于可以在以后短路的无限数据源。
  3. 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,28

         

    93
      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;)。

  1. 将您的dataSrc Supplier<Stream<T>>变为收集信息流的Supplier<List<T>>
  2. Suppliers#memoize
  3. 换行

    这将是您的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>操作。

关于原始帖子的示例,其中tempsCompletableFuture<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)第一次遍历以收集缓存中的项目。