How to map RuntimeExceptions in Java streams to “recover” from invalid stream elements

前端 未结 2 465
轮回少年
轮回少年 2021-01-01 04:46

Imagine I\'m building a library, that will receive a Stream of Integers, and all the library code needs to do is return a stream of Strings with the string representation of

2条回答
  •  粉色の甜心
    2021-01-01 05:40

    Note: Please see the edit at the end of this post, which fixes a bug in my original answer. I'm leaving my original answer anyway, because it's still useful for many cases and I think it helps solve OP's question, at least with some restrictions.


    Your approach with Iterator goes in the right direction. The solution might be drafted as follows: convert the stream to an iterator, wrap the iterator as you have already done, and then create a stream from the wrapper iterator, except that you should use a Spliterator instead. Here's the code:

    private static  Stream asNonThrowingStream(
            Stream stream,
            Supplier valueOnException) {
    
        // Get spliterator from original stream
        Spliterator spliterator = stream.spliterator();
    
        // Return new stream from wrapper spliterator
        return StreamSupport.stream(
    
            // Extending AbstractSpliterator is enough for our purpose
            new Spliterators.AbstractSpliterator(
                    spliterator.estimateSize(),
                    spliterator.characteristics()) {
    
                // We only need to implement tryAdvance
                @Override
                public boolean tryAdvance(Consumer action) {
                    try {
                        return spliterator.tryAdvance(action);
                    } catch (RuntimeException e) {
                        action.accept(valueOnException.get());
                        return true;
                    }
                }
            }, stream.isParallel());
    }
    

    We are extending AbstractSpliterator to wrap the spliterator returned by the original stream. We only need to implement the tryAdvance method, which either delegates to the original spliterator's tryAdvance method, or catches RuntimeException and invokes the action with the supplied valueOnException value.

    Spliterator's contract specifies that the return value of tryAdvance must be true if the action is consumed, so if a RuntimeException is catched, it means that the original spliterator has thrown it from within its own tryAdvance method. Thus, we return true in this case, meaning that the element was consumed anyway.

    The original spliterator's estimate size and characteristics are preserved by passing these values as arguments to the constructor of AbstractSpliterator.

    Finally, we create a new stream from the new spliterator via the StreamSupport.stream method. The new stream is parallel if the original one was also parallel.

    Here's how to use the above method:

    public Stream convertToString(Stream input) {
        return asNonThrowingStream(input.map(String::valueOf), () -> "NaN");
    }
    

    Edit

    As per Holger's comment below, user holi-java has kindly provided a solution that avoids the pitfalls pointed out by Holger.

    Here's the code:

     Stream exceptionally(Stream source, BiConsumer> handler) {
        class ExceptionallySpliterator extends AbstractSpliterator
                implements Consumer {
    
            private Spliterator source;
            private T value;
            private long fence;
    
            ExceptionallySpliterator(Spliterator source) {
                super(source.estimateSize(), source.characteristics());
                this.fence = source.getExactSizeIfKnown();
                this.source = source;
            }
    
            @Override
            public Spliterator trySplit() {
                Spliterator it = source.trySplit();
                return it == null ? null : new ExceptionallySpliterator(it);
            }
    
            @Override
            public boolean tryAdvance(Consumer action) {
                return fence != 0 && consuming(action);
            }
    
            private boolean consuming(Consumer action) {
                Boolean state = tryConsuming(action);
                if (state == null) {
                    return true;
                }
                if (state) {
                    action.accept(value);
                    value = null;
                    return true;
                }
                return false;
            }
    
    
            private Boolean tryConsuming(Consumer action) {
                fence--;
                try {
                    return source.tryAdvance(this);
                } catch (Exception ex) {
                    handler.accept(ex, action);
                    return null;
                }
            }
    
            @Override
            public void accept(T value) {
                this.value = value;
            }
        }
    
        return stream(new ExceptionallySpliterator(source.spliterator()), source.isParallel()).onClose(source::close);
    }
    

    Please refer to the tests if you want to further know about this solution.

提交回复
热议问题