Reputation: 3559
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
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