Итератор Java 8 для потоковой передачи в итератор вызывает избыточный вызов hasNext()

Я заметил немного странное поведение в следующем сценарии:

Итератор -> Поток -> карта() -> итератор() -> итерация

hasNext() исходного итератора вызывается еще раз после того, как он уже вернул false.

Это нормально?

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!!!!

person Nazaret K.    schedule 13.03.2015    source источник
comment
Я бы не назвал это «нормальным», но в рамках спецификации.   -  person Holger    schedule 13.03.2015
comment
Вы имеете в виду, что Iterator.hasNext() должен быть идемпотентным, верно?   -  person Nazaret K.    schedule 14.03.2015
comment
Верно, next можно вызывать столько раз, сколько пожелает вызывающий...   -  person Holger    schedule 16.03.2015


Ответы (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.

person Sean Van Gorder    schedule 21.04.2015