Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any danger in making the action .accept() more than one element in an implementation of Spliterator's .tryAdance()?

The javadoc of Spliterator mentions that:

A Spliterator may traverse elements individually (tryAdvance()) or sequentially in bulk (forEachRemaining()).

Then we go to the javadoc of tryAdvance() which says that:

If a remaining element exists, performs the given action on it, returning true; else returns false.

Maybe I am misreading somewhere, but to me it seems that provided there is one element, or more, remaining, the Consumer as an argument should only every .accept() one argument before returning true, and that if, say, I have two arguments immediately available, then I cannot:

action.accept(arg1);
action.accept(arg2);
return true;

In this project, I have rewritten the breadth first spliterator so that it now reads:

// deque is a Deque<Iterator<T>>

@Override
public boolean tryAdvance(final Consumer<? super T> action)
{
    Iterator<T> iterator;
    T element;

    while (!deque.isEmpty()) {
        iterator = deque.removeFirst();
        while (iterator.hasNext()) {
            element = iterator.next();
            deque.add(fn.apply(element));
            action.accept(element);
        }
    }
    return false;
}

In short, I make the action accept all arguments, and then return false... and the test, albeit quite simple, still succeeds (link).

Note that .trySplit() always returns null; and the spliterator has chacacteristics DISTINCT, ORDERED and NONNULL.

So, is there a stream usage in which the code above will not work due to the method above consuming all elements at once?

like image 386
fge Avatar asked Apr 28 '16 17:04

fge


1 Answers

Your assumption that tryAdvance() should only consume one element is right. This, however, does not imply that you will notice violations of the contract immediately. When you test using operations like .collect(Collectors.toList()) it’s even unlikely to spot such a violation as most operations consuming all elements will invoke forEachRemaining() on the spliterator whose default implementation is documented as:

The default implementation repeatedly invokes tryAdvance(java.util.function.Consumer) until it returns false.

Obviously, for that method it makes no difference.

The Stream framework will invoke tryAdvance() when performing lazy operations. So when you use .peek(System.out::println).findFirst() you may notice a difference when your tryAdvance() implementation pushes more than one value. Still, given the current implementation, the result is the correct first element. Apparently, the implementation-provided consumer ignores subsequent values after encountering a value.

This might be connected to other implementation details like the one discussed in “Why filter() after flatMap() is ‘not completely’ lazy in Java streams?”. If the stream implementation itself pushes more values than necessary under certain circumstances, the receiving end in the same implementation must be prepared to handle that case.

But it must be emphasized that this is the behavior of one particular Stream API implementation. A different implementation or the next Java version might rely on a correct implementation of tryAdvance. Further, there might be use cases for Spliterators other than Streams.


Well, I finally found an example of an operation that breaks with your Spliterator:

for(Iterator<?> it=Spliterators.iterator(spliterator); it.hasNext();) {
    System.out.println(it.next());
}
like image 121
Holger Avatar answered Sep 28 '22 09:09

Holger