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

前端 未结 2 448
轮回少年
轮回少年 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:39

    The error occurs in the stream intermediate operation, a clever way like as you to solving the problem is using the Proxy Design Pattern. for using the stream api you just need to proxying an Iterator from the source Stream to another Stream by StreamSupport#stream & Spliterators#spliterator(Iterator, long, int) , for example:

    Stream<String> result = convertToString(Stream.of("1", "bad", "2")
                           .map(Integer::parseInt));
    
    
    
    public Stream<String> convertToString(Stream<Integer> input) {
        return exceptionally(input, (e, action) -> action.accept(null))
                .map(it -> String.format("%s", it == null ? "NaN" : it));
    }
    

    Current version Stream is base on Iterator that fixed the Stream.of(T) bug, for more details please see my question.

    <T> Stream<T> exceptionally(Stream<T> source,
                                BiConsumer<Exception, Consumer<? super T>> handler) {
        Spliterator<T> s = source.spliterator();
        return StreamSupport.stream(
                spliterator(
                        exceptionally(s, handler),
                        s.estimateSize(),
                        s.characteristics()
                ),
                source.isParallel()
        ).onClose(source::close);
    }
    
    
    //Don't worried the thread-safe & robust since it is invisible for anyone
    private <T> Iterator<T> exceptionally(Spliterator<T> spliterator,
                                BiConsumer<Exception, Consumer<? super T>> handler) {
        class ExceptionallyIterator implements Iterator<T>, Consumer<T> {
            private Iterator<T> source = Spliterators.iterator(spliterator);
            private T value;
            private boolean valueInReady = false;
            private boolean stop = false;
    
            @Override
            public boolean hasNext() {
    
                while (true) {
                    if (valueInReady) return true;
                    if (stop) return false;
                    try {
                        return source.hasNext();
                    } catch (Exception ex) {
                        stop = shouldStopTraversing(ex);
                        handler.accept(ex, this);
                    }
                }
            }
    
            @Override
            public T next() {
                return valueInReady ? dump() : source.next();
            }
    
            private T dump() {
                T result = value;
                valueInReady = false;
                value = null;
                return result;
            }
    
            @Override
            public void accept(T value) {
                this.value = value;
                this.valueInReady = true;
            }
        }
        return new ExceptionallyIterator();
    }
    

    static final String BUG_CLASS = "java.util.stream.Streams$StreamBuilderImpl";
    
    public static boolean shouldStopTraversing(Exception ex) {
        for (StackTraceElement element : ex.getStackTrace()) {
            if (BUG_CLASS.equals(element.getClassName())) {
                return true;
            }
        }
        return false;
    }
    
    0 讨论(0)
  • 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 <T> Stream<T> asNonThrowingStream(
            Stream<T> stream,
            Supplier<? extends T> valueOnException) {
    
        // Get spliterator from original stream
        Spliterator<T> spliterator = stream.spliterator();
    
        // Return new stream from wrapper spliterator
        return StreamSupport.stream(
    
            // Extending AbstractSpliterator is enough for our purpose
            new Spliterators.AbstractSpliterator<T>(
                    spliterator.estimateSize(),
                    spliterator.characteristics()) {
    
                // We only need to implement tryAdvance
                @Override
                public boolean tryAdvance(Consumer<? super T> 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<String> convertToString(Stream<Integer> 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:

    <T> Stream<T> exceptionally(Stream<T> source, BiConsumer<Exception, Consumer<? super T>> handler) {
        class ExceptionallySpliterator extends AbstractSpliterator<T>
                implements Consumer<T> {
    
            private Spliterator<T> source;
            private T value;
            private long fence;
    
            ExceptionallySpliterator(Spliterator<T> source) {
                super(source.estimateSize(), source.characteristics());
                this.fence = source.getExactSizeIfKnown();
                this.source = source;
            }
    
            @Override
            public Spliterator<T> trySplit() {
                Spliterator<T> it = source.trySplit();
                return it == null ? null : new ExceptionallySpliterator(it);
            }
    
            @Override
            public boolean tryAdvance(Consumer<? super T> action) {
                return fence != 0 && consuming(action);
            }
    
            private boolean consuming(Consumer<? super T> 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<? super T> 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.

    0 讨论(0)
提交回复
热议问题