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

家住魔仙堡 提交于 2019-12-08 23:32:39

问题


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 the number.

public Stream<String> convertToString(Stream<Integer> input) {
  return input.map(n -> String.valueOf(n));
}

However, imagine someone decides to invoke my function with the following code:

Stream<Integer> input = Stream.of(1,2,3)
  .map(n -> {
    if (n %3 ) { throw new RuntimeException("3s are not welcome here!"); }
    else { return n; }
  }

Stream<String> output = convertToString(input);

Since I want my API contract to be one that can handle this situation and still return a Stream of Strings, I'd like to know how to change my code so it detects that there's an exception in the stream and "recover" from the exception by mapping it to a special "NaN" value.

In the end, I want my output Stream to be "1","2","NaN"

Notes:

  • I don't want my API to have to define a special wrapper for the input.
  • I don't want to rely on a 3rd party library due to implications of dependencies.
  • I don't know how big the stream will be or at what rate it will be generated, so I can't use any sort of caching/preloading of all values.

Is this possible with Java 8 Streams?

With an Iterator, I imagine I could have done:

public class SafeIntegerToStringIterator implements Iterator<String> {
  Iterator<Integer> innerIterator;
  ...
  public String next() throws NoSuchElementException {
    try { return String.valueOf(this.innerIterator.next()); }
    catch (RuntimeException e) { return "NaN"; }
  }

}
...
public Iterator<String> convertToString(Iterator<Integer> input) {
  return new SafeIntegerToStringIterator(input);
}

Thanks


回答1:


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.




回答2:


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


来源:https://stackoverflow.com/questions/44616583/how-to-map-runtimeexceptions-in-java-streams-to-recover-from-invalid-stream-el

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!