Nazaret K.
Nazaret K.

Reputation: 3559

Java 8 Iterator to stream to iterator causes redundant call to hasNext()

I notice a bit of a strange behavior in the following scenario:

Iterator -> Stream -> map() -> iterator() -> iterate

The hasNext() of the original iterator is called an additional time after having already returned false.

Is this normal?

package com.test.iterators;

import java.util.Iterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class TestIterator {

    private static int counter = 2;

    public static void main(String[] args) {

        class AdapterIterator implements Iterator<Integer> {
            boolean active = true;

            @Override
            public boolean hasNext() {
                System.out.println("hasNext() called");

                if (!active) {
                    System.out.println("Ignoring duplicate call to hasNext!!!!");
                    return false;
                }

                boolean hasNext = counter >= 0;
                System.out.println("actually has next:" + active);

                if (!hasNext) {
                    active = false;
                }

                return hasNext;
            }

            @Override
            public Integer next() {
                System.out.println("next() called");
                return counter--;
            }
        }

        Stream<Integer> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(new AdapterIterator(), 0), false);
        stream.map(num -> num + 1).iterator().forEachRemaining(num -> {
            System.out.println(num);
        });
    }
}

If I either remove the map() or replace the final itearator() with something like count() or collect() it works without the redundant call.

Output

hasNext() called
actually has next:true
next() called
3
hasNext() called
actually has next:true
next() called
2
hasNext() called
actually has next:true
next() called
1
hasNext() called
actually has next:true
hasNext() called
Ignoring duplicate call to hasNext!!!!

Upvotes: 5

Views: 1430

Answers (1)

Sean Van Gorder
Sean Van Gorder

Reputation: 3453

Yes, this is normal. The redundant call happens in StreamSpliterators.AbstractWrappingSpliterator.fillBuffer(), which is called from the hasNext() method of the iterator returned by stream.map(num -> num + 1).iterator(). From the JDK 8 source:

/**
 * If the buffer is empty, push elements into the sink chain until
 * the source is empty or cancellation is requested.
 * @return whether there are elements to consume from the buffer
 */
private boolean fillBuffer() {
    while (buffer.count() == 0) {
        if (bufferSink.cancellationRequested() || !pusher.getAsBoolean()) {
            if (finished)
                return false;
            else {
                bufferSink.end(); // might trigger more elements
                finished = true;
            }
        }
    }
    return true;
}

The call to pusher.getAsBoolean() calls hasNext() on the original AdapterIterator instance. If true, it adds the next element to bufferSink and returns true, otherwise it returns false. When the original iterator runs out of items and it returns false, this method calls bufferSink.end() and retries filling the buffer, which leads to the redundant hasNext() call.

In this case, bufferSink.end() has no effect and the second attempt to fill the buffer is unnecessary, but as the source comment explains, it "might trigger more elements" in another situation. This is just an implementation detail buried deep in the complex inner workings of Java 8 streams.

Upvotes: 1

Related Questions