繁体   English   中英

注册流“完成”挂钩

[英]Register a Stream "completion" hook

使用 Java 8 Stream API,我想注册一个“完成挂钩”,如下所示:

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));
});

我想要这样做的原因是因为我想将资源包装在Stream中供 API 客户端使用,并且我希望Stream在资源被使用后自动清理资源。 如果可能的话,那么客户可以调用:

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();
    }
}

有没有一种简单的方法可以使用现有的 JDK 8 API 来做到这一点?

除了基于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 在这种情况下,完成将由垃圾收集器触发(这也有一些缺点)。

总之,我的建议是继续使用 try-with-resources。

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

// 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操作

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

但是当前的实现并不像使用flatMap时应该的那样懒惰 这已在 Java 10 中修复。


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

Java 8 已经有了需要关闭流的操作方式的先例。 在他们的Javadoc中,它提到:

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

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

这就是 API 设计和文档建议的方式来执行您正在尝试执行的操作。

我提出的解决方案如下所示:

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;
    }
}

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

在开源项目 Speedment 上查看 AutoClosingReferenceStream、AutoClosingIntStream、AutoClosingLongStream 和 AutoClosingDoubleStream 的这些完整实现https://github.com/speedment/speedment/tree/master/src/main/java/com/speedment/internal/core/流/自动关闭

该解决方案类似于@LukasEder 提到的解决方案

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM