简体   繁体   English

相当于(RxJava)Observable#onComplete()的Java 8

[英]Java 8 equivalent of (RxJava) Observable#onComplete()

I'm getting to know Java 8 Stream API and I am unsure how to signal to a consumer of a stream that the stream is completed. 我逐渐了解Java 8 Stream API,并且不确定如何向流的使用者发送信号,告知该流已完成。

In my case the results of the stream-pipeline will be written in batches to a database or published on a messaging service. 就我而言,流管道的结果将成批写入数据库或在消息传递服务上发布。 In such cases the stream-pipeline should invoke a method to "flush" and "close" the endpoints once the stream is closed. 在这种情况下,一旦关闭流,流管道应调用一种方法来“刷新”和“关闭”端点。

I had a bit of exposure to the Observable pattern as implemented in RxJava and remember the Observer#onComplete method is used there for this purpose. 我对RxJava中实现的Observable模式有一些了解,并记得在其中使用了Observer#onComplete方法。

On the other hand Java 8 Consumer only exposes an accept method but no way to "close" it. 另一方面,Java 8 Consumer仅公开了accept方法,而没有“关闭”方法。 Digging in the library I found a sub-interface of Consumer called Sink which offers an end method, but it's not public. 在库中进行挖掘时,我发现了一个名为SinkConsumer子接口,它提供了一种end方法,但它不是公开的。 Finally I thought of implementing a Collector which seems to be the most flexible consumer of a stream, but isn't there any simpler option? 最后,我想到了实现一个似乎是流中最灵活的使用者的Collector ,但是没有任何更简单的选择吗?

The simplest way of doing a final operation is by placing the appropriate statement right after the terminal operation of the stream, for example: 完成最终操作的最简单方法是在流的终端操作之后放置适当的语句,例如:

IntStream.range(0, 100).parallel().forEach(System.out::println);
System.out.println("done");

This operation will be performed in the successful case only, where a commit is appropriate. 仅在成功的情况下,才需要执行此操作。 While the Consumer s run concurrently, in unspecified order, it is still guaranteed that all of them have done their work upon normal return. 尽管Consumer以未指定的顺序并发运行,但仍可以保证它们在正常返回时都已完成工作。

Defining an operation that is also performed in the exceptional case is not that easy. 定义在特殊情况下也要执行的操作并不是那么容易。 Have a look at the following example: 看下面的例子:

try(IntStream is=IntStream.range(0, 100).onClose(()->System.out.println("done"))) {
    is.parallel().forEach(System.out::println);
}

This works like the first one but if you test it with an exceptional case, eg 它的工作原理与第一个类似,但是如果您在特殊情况下进行测试,例如

try(IntStream is=IntStream.range(0, 100).onClose(()->System.out.println("done"))) {
    is.parallel().forEach(x -> {
        System.out.println(x);
        if(Math.random()>0.7) throw new RuntimeException();
    });
}

you might encounter printouts of numbers after done . 可能遇到的数字打印done This applies to all kind of cleanup in the exceptional case. 在特殊情况下,这适用于所有类型的清理。 When you catch the exception or process a finally block, there might be still running asynchronous operations. 当您捕获异常或处理finally块时,可能仍在运行异步操作。 While it is no problem rolling back a transaction in the exceptional case at this point as the data is incomplete anyway, you have to be prepared for still running attempts to write items to the now-rolled-back resource. 尽管此时由于数据不完整而在特殊情况下回滚事务没有问题,但您仍必须为仍在运行的尝试将项目写入现在回滚的资源做好准备。

Note that Collector -based solutions, which you thought about, can only define a completion action for the successful completion. 请注意,您所考虑的基于Collector的解决方案只能为成功完成定义一个完成操作。 So these are equivalent to the first example; 因此,这些等同于第一个示例; just placing the completing statement after the terminal operation is the simpler alternative to the Collector . 仅将完成语句放置在终端操作之后是Collector的更简单选择。


If you want to define operations which implement both, the item processing and the clean up steps, you may create your own interface for it and encapsulate the necessary Stream setup into a helper method. 如果要定义同时实现项目处理和清理步骤的操作,则可以为其创建自己的接口,并将必要的Stream设置封装到一个辅助方法中。 Here is how it might look like: 这可能是这样的:

Operation interface: 操作界面:

interface IoOperation<T> {
    void accept(T item) throws IOException;
    /** Called after successfull completion of <em>all</em> items */
    default void commit() throws IOException {}
    /**
     * Called on failure, for parallel streams it must set the consume()
     * method into a silent state or handle concurrent invocations in
     * some other way
     */
    default void rollback() throws IOException {}
}

Helper method implementation: 辅助方法的实现:

public static <T> void processAllAtems(Stream<T> s, IoOperation<? super T> c) 
throws IOException {
    Consumer<IoOperation> rollback=io(IoOperation::rollback);
    AtomicBoolean success=new AtomicBoolean();
    try(Stream<T> s0=s.onClose(() -> { if(!success.get()) rollback.accept(c); })) {
        s0.forEach(io(c));
        c.commit();
        success.set(true);
    }
    catch(UncheckedIOException ex) { throw ex.getCause(); }
}
private static <T> Consumer<T> io(IoOperation<T> c) {
    return item -> {
        try { c.accept(item); }
        catch (IOException ex) { throw new UncheckedIOException(ex); }
    };
}

Using it without error handling might be as easy as 在没有错误处理的情况下使用它可能像

class PrintNumbers implements IoOperation<Integer> {
    public void accept(Integer i) {
        System.out.println(i);
    }
    @Override
    public void commit() {
        System.out.println("done.");
    }
}
processAllAtems(IntStream.range(0, 100).parallel().boxed(), new PrintNumbers());

Dealing with errors is possible, but as said, you have to handle the concurrency here. 处理错误是可能的,但是如上所述,您必须在此处处理并发。 The following example does also just print number but use a new output stream that should be closed at the end, therefore the concurrent accept calls have to deal with concurrently closed streams in the exceptional case. 以下示例也仅打印编号,但使用了应在最后关闭的新输出流,因此,在特殊情况下,并发accept调用必须处理并发关闭的流。

class WriteNumbers implements IoOperation<Integer> {
    private Writer target;
    WriteNumbers(Writer writer) {
        target=writer;
    }
    public void accept(Integer i) throws IOException {
        try {
            final Writer writer = target;
            if(writer!=null) writer.append(i+"\n");
            //if(Math.random()>0.9) throw new IOException("test trigger");
        } catch (IOException ex) {
            if(target!=null) throw ex;
        }
    }
    @Override
    public void commit() throws IOException {
        target.append("done.\n").close();
    }
    @Override
    public void rollback() throws IOException {
        System.err.print("rollback");
        Writer writer = target;
        target=null;
        writer.close();
    }
}
FileOutputStream fos = new FileOutputStream(FileDescriptor.out);
FileChannel fch = fos.getChannel();
Writer closableStdIO=new OutputStreamWriter(fos);
try {
    processAllAtems(IntStream.range(0, 100).parallel().boxed(),
                    new WriteNumbers(closableStdIO));
} finally {
    if(fch.isOpen()) throw new AssertionError();
}

Terminal operations on Java 8 streams (like collect() , forEach() etc) will always complete the stream. Java 8流上的终端操作(例如collect()forEach()等)将始终完成该流。

If you have something that is processing objects from the Stream you know when the stream ends when the Collector returns. 如果您正在处理流中的对象,则知道收集器返回时流结束。

If you just have to close your processor, you can wrap it in a try-with-resource and perform the terminal operatoni inside the try block 如果只需要关闭处理器,则可以将其包装在try-with-resource中,并在try块内执行终端操作

try(BatchWriter writer = new ....){

       MyStream.forEach( o-> writer.write(o));

 }//autoclose writer

You can use Stream#onClose(Runnable) to specify a callback to invoke when the stream is closed. 您可以使用Stream#onClose(Runnable)指定在关闭流时要调用的回调。 Streams are pull-based (contrary to push-based rx.Observable), so the hook is associated with stream, not it's consumers 流是基于拉的(与基于推的rx.Observable相反),因此钩子与流相关联,而不是它的使用者

This is a bit of a hack but works well. 这有点hack,但效果很好。 Create a stream concatenation of the original + a unique object. 创建原始+唯一对象的流连接。

Using peek(), see if the new object is encountered, and call the onFinish action. 使用peek(),查看是否遇到新对象,然后调用onFinish操作。

Return the stream with filter, so that the unique object won't be returned. 使用过滤器返回流,这样就不会返回唯一对象。

This preserves the onClose event of the original stream. 这将保留原始流的onClose事件。

public static <T> Stream<T> onFinish(Stream<T> stream, Runnable action) {
    final Object end = new Object(); // unique object

    Stream<Object> withEnd = Stream.concat(stream.sequential(), Stream.of(end));
    Stream<Object> withEndAction = withEnd.peek(item -> {
        if (item == end) {
            action.run();
        }
    });
    Stream<Object> withoutEnd = withEndAction.filter(item -> item != end);
    return (Stream<T>) withoutEnd;
}

Another option is to wrap the original spliterator, and when it returns false, call the action. 另一种选择是包装原始分隔符,并在返回false时调用该操作。

public static <T> Stream<T> onFinishWithSpliterator(Stream<T> source, Runnable onFinishAction) {
    Spliterator<T> spliterator = source.spliterator();

    Spliterator<T> result = new Spliterators.AbstractSpliterator<T>(source.estimateSize(), source.characteristics()) {
        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean didAdvance = source.tryAdvance(action);
            if (!didAdvance) {
                onFinishAction.run();
            }
            return didAdvance;
        }
    };

    // wrap the the new spliterator with a stream and keep the onClose event
    return StreamSupport.stream(result, false).onClose(source::close);

}

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

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