Java 8 Iterator для потоковой передачи на итератор вызывает избыточный вызов hasNext()
Я наблюдаю немного странное поведение в следующем сценарии:
Iterator → Stream → map() → iterator() → iterate
Исходный итератор hasNext() называется дополнительным временем после того, как он уже вернулся.
Это нормально?
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);
});
}
}
Если я либо удаляю map(), либо заменяю конечный itearator() чем-то вроде count() или collect(), он работает без избыточного вызова.
Выход
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!!!!
Ответы
Ответ 1
Да, это нормально. Резервированный вызов происходит в StreamSpliterators.AbstractWrappingSpliterator.fillBuffer()
, который вызывается из метода hasNext()
итератора, возвращаемого stream.map(num -> num + 1).iterator()
. Из источника JDK 8:
/**
* 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;
}
Вызов pusher.getAsBoolean()
вызывает hasNext()
в исходном экземпляре AdapterIterator
. Если true, он добавляет следующий элемент в bufferSink
и возвращает true, иначе он возвращает false. Когда исходный итератор исчерпывает элементы и возвращает false, этот метод вызывает bufferSink.end()
и повторяет заполнение буфера, что приводит к избыточному вызову hasNext()
.
В этом случае bufferSink.end()
не действует, а вторая попытка заполнить буфер необязательна, но, как объясняет исходный комментарий, он может "вызвать больше элементов" в другой ситуации. Это всего лишь деталь реализации, глубоко погруженная в сложную внутреннюю работу потоков Java 8.