注册流“完成”钩子

时间:2016-01-12 20:22:19

标签: java java-8 java-stream

使用Java 8 Stream<String> stream = Stream.of("a", "b", "c"); // additional filters / mappings that I don't control stream.onComplete((Completion c) -> { // This is what I'd like to do: closeResources(); // This might also be useful: Optional<Throwable> exception = c.exception(); exception.ifPresent(e -> throw new ExceptionWrapper(e)); }); API,我希望注册一个“完成挂钩”,类似于:

Stream

我之所以这样做是因为我想在Stream中包装资源以供API客户端使用,我希望Collected collectedInOneGo = Utility.something() .niceLookingSQLDSL() .moreDSLFeatures() .stream() .filter(a -> true) .map(c -> c) .collect(collector); 一旦自动清理资源{}消耗。如果可能,那么客户可以致电:

try (Stream<X> meh = Utility.something()
                            .niceLookingSQLDSL()
                            .moreDSLFeatures()
                            .stream()) {

    Collected collectedWithUglySyntacticDissonance =
    meh.filter(a -> true)
       .map(c -> c)
       .collect(collector);
}

而不是目前所需要的:

java.util.stream.ReferencePipeline

理想情况下,我想进入@Override final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) { try { // Existing loop do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink)); } // These would be nice: catch (Throwable t) { completion.onFailure(t); } finally { completion.onSuccess(); } } 的各种方法,例如:

class AutoClosingStream<T> implements Stream<T> {

    AutoClosingStream(Stream<T> delegate, Consumer<Optional<Throwable>> onComplete) {}

    // Pipeline ops delegate the op to the real stream and wrap that again
    @Override
    public Stream<T> limit(long maxSize) {
        return new AutoClosingStream(delegate.limit(maxSize), onComplete);
    }

    // Terminal ops intercept the result and call the onComplete logic
    @Override
    public void forEach(Consumer<? super T> action) {
        terminalOp(() -> delegate.forEach(action));
    }

    private void terminalOp(Runnable runnable) {
        terminalOp(() -> { runnable.run(); return null; });
    }

    private <R> R terminalOp(Supplier<R> supplier) {
        R result = null;

        try {
            result = supplier.get();
            onComplete.accept(Optional.empty());
        }
        catch (Throwable e) {
            onComplete.accept(Optional.of(e));
            Utils.sneakyThrow(e);
        }

        return result;
    }
}

使用现有的JDK 8 API有一种简单的方法吗?

5 个答案:

答案 0 :(得分:11)

任何拦截终端操作的解决方案除了基于flatMap的解决方案(由@Holger提出)都会对以下代码脆弱:

Stream<String> stream = getAutoCloseableStream();
if(stream.iterator().hasNext()) {
    // do something if stream is non-empty
}

这种用法在规范中绝对合法。不要忘记iterator()spliterator()是终端流操作,但在执行后仍然需要访问流源。在任何州放弃IteratorSpliterator也是完全有效的,所以你无法知道它是否会被进一步使用。

您可以考虑建议用户不要使用iterator()spliterator(),但这段代码呢?

Stream<String> stream = getAutoCloseableStream();
Stream.concat(stream, Stream.of("xyz")).findFirst();

这在内部使用spliterator().tryAdvance()作为第一个流,然后放弃它(尽管在显式调用结果流close()时关闭)。您还需要让用户不要使用Stream.concat。据我所知,您在图书馆内部经常使用iterator() / spliterator(),因此您需要重新访问所有这些地方以解决可能出现的问题。当然,还有很多其他库也使用iterator() / spliterator(),之后可能会短路:所有这些库都会与您的功能不兼容。

为什么基于flatMap的解决方案适用于此处?因为在第一次调用hasNext()tryAdvance()时,它会将整个流内容转储到中间缓冲区并关闭原始流源。因此,根据流的大小,您可能会浪费很多中间内存,甚至可能会OutOfMemoryError

您还可以考虑将PhantomReference保留到Stream个对象并监控ReferenceQueue。在这种情况下,完成将由垃圾收集器触发(这也有一些缺点)。

总之,我的建议是继续尝试使用资源。

答案 1 :(得分:10)

最简单的解决方案是将流包装在另一个流中并将其平面映射到自身:

// example stream
Stream<String> original=Stream.of("bla").onClose(()->System.out.println("close action"));

// this is the trick
Stream<String> autoClosed=Stream.of(original).flatMap(Function.identity());

//example op
int sum=autoClosed.mapToInt(String::length).sum();
System.out.println(sum);

它起作用的原因在于flatMap operation

  

每个映射的流在将其内容放入此流后将关闭。

但是the current implementation isn’t as lazy as it should be when using flatMap。这已在Java 10中修复。

我的建议是在需要关闭返回的流时继续使用try(…)标准解决方案和文档。毕竟,在终端操作之后关闭资源的流是不安全的,因为没有保证客户端实际上将调用终端操作。改变它的想法并放弃流即时是一种有效的用法,而当文档指定它是必需的时,不调用close()方法则不是。

答案 2 :(得分:6)

Java 8已经有了一个关于需要关闭的流如何运行的先例。在他们的Javadoc中,它提到:

  

Streams有一个BaseStream.close()方法并实现AutoCloseable,但几乎所有流实例实际上都不需要在使用后关闭。通常,只有源为IO通道的流(例如Files.lines(Path,Charset)返回的流)才需要关闭。大多数流都由集合,数组或生成函数支持,不需要特殊的资源管理。 (如果流确实需要关闭,则可以在try-with-resources语句中将其声明为资源。)

所以Java 8的建议是在try-with-resources中打开这些流。一旦你这样做,Stream 提供了一种方法来添加一个关闭钩子,几乎完全如你所描述的那样:onClose(Runnable),它接受​​一个lambda告诉它做什么并返回Stream,当它关闭时也会执行该操作。

这就是API设计和文档建议你做你想做的事情的方式。

答案 3 :(得分:2)

我提出的解决方案如下:

IntStream

这只是一个简化的草图来说明这个想法。真正的解决方案还将支持原始LongStreamDoubleStreamauthenticate!

答案 4 :(得分:1)

在开源项目Speedment https://github.com/speedment/speedment/tree/master/src/main/java/com/speedment/internal/core/stream/autoclose

中查看AutoClosingReferenceStream,AutoClosingIntStream,AutoClosingLongStream和AutoClosingDoubleStream的这些完整实现。

解决方案类似于@LukasEder

提到的解决方案