AneeshMohan0
AneeshMohan0

Reputation: 13

Java 9 Stream.iterate​(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next) call's hasNext after calling next

My understanding is it needs to check hasNext and then if it's true, then call the next method to get the element in the iterator.

Issue:

It calls hasNext after retrieving the element, In my case, the last element is retrieved using next and checks for hasNext then it returns false and the loop ends. So I process N-1 elements instead of N elements.

Alternatively, I have used the below code.

Stream.generate(() -> j)
    .takeWhile(SlidingWindow::hasNextTimePeriod)
    .map(slider -> slider.next())

Reproducing Code :

@Slf4j
class Java9StreamTest {

    @Test
    void testStream() {

        final SlidingWindows slidingWindows = new SlidingWindows();

        Stream.iterate(slidingWindows.next(), j -> slidingWindows.hasNext(), j -> slidingWindows.next())
                .forEach(value -> log.info("{}", value));
    }
}

class SlidingWindows {
    private final List<String> list = List.of("A", "B", "C");
    private int count = 0;

    public boolean hasNext() {
        return count < list.size();
    }

    public String next() {
        final String result = list.get(count);
        count = count + 1;
        return result;
    }    
}

Output :

20:20:03.485 [main] INFO mohan.stream.Java9StreamTest - A
20:20:03.489 [main] INFO mohan.stream.Java9StreamTest - B

Missing last element C

It works like a for loop, It's misleading in Java API to say hasNext and next in Docs.

What is equivalent to the below code in Streams?

 while(slidingWindows.hasNext()) { 
    final String next = slidingWindows.next(); 
    log.info("{}", next); 
 }

Upvotes: 1

Views: 771

Answers (1)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29038

According to the documentation Stream.iterate() utilizes for loop under the hood:

Stream.iterate should produce the same sequence of elements as produced by the corresponding for-loop:

 for (T index=seed; hasNext.test(index); index = next.apply(index)) {
     ...
 }  

The resulting sequence may be empty if the hasNext predicate does not hold on the seed value. Otherwise the first element will be the supplied seed value, the next element (if present) will be the result of applying the next function to the seed value, and so on iteratively until the hasNext predicate indicates that the stream should terminate.

So basically the first argument of the iterate() seed corresponds to the initialization expression of the for loop, the second argument (predicate) acts as the termination expression and the third argument (unary operator) plays the role of the increment expression (see).

Examples

Let's have a look at the couple of examples:

Iterator<String> iter = List.of("A", "B", "C").iterator();

Stream.iterate(iter.next(), str -> iter.hasNext(), str -> iter.next())
    .forEach(System.out::println);

Which is does the same as:

Stream.generate(() -> "dummy")
    .map(str -> iter.next()) // requesting the element
    .peek(str -> System.out.println(str + " <- invoking next()"))
    .takeWhile(str -> {
        boolean result = iter.hasNext(); // checking whether the next element exists
        System.out.println(result + " <- invoking hasNext()");
        return result;
    })
    .forEach(str -> System.out.println(str + " <- has reached the terminal operation\n"));

Output of the second stream:

A <- invoking next()
true <- invoking hasNext()
A <- has reached the terminal operation

B <- invoking next()
true <- invoking hasNext()
B <- has reached the terminal operation

C <- invoking next()
false <- invoking hasNext()

Let's explore all actions:

  • The seed iter.next() will retrieve the first element A (in the second stream it will be done by the map operation). Then the predicate iter.next() would be triggered.

  • Then the second element B would be retrieved, and the predicate will come into play.

  • Then the third element C would follow, and the predicate iter.next() will return false because the source was already exhausted.

Conclusion : in both cases, the last element C gets grabbed from the source before the predicate gets executed and since the source is already empty the predicate would be evaluated to false and this element would be thrown away.

How to fix it?

To make it successfully retrieve all the elements from the source, you need to swap the map and takeWhile, i.e. first check whether there's a next element and only then request the next element:

Iterator<String> iter = List.of("A", "B", "C").iterator();

Stream.generate(() -> "dummy")
    .takeWhile(str -> iter.hasNext())
    .map(str -> iter.next())
    .forEach(System.out::println);

Output:

A
B
C

This fix is only applicable with combination takeWhile + map, but we can't change the behavior of the iterate().


As @Slow has mentioned in the comments, if you make the SlidingWindows class implement Iterator interface, then you can make use of the StreamSupport utility class that will take care of translating all the elements into a stream for you as it has been described here: Why does Iterable not provide stream() and parallelStream() methods?

Note that it also requires transforming the iterator into an instance of Spliterator which can be done by utilizing the static method spliteratorUnknownSize() of the Spliterators utility class.

StreamSupport.stream(
        Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), 
        false) // a flag that denotes whether the stream should be parallel
    .forEach(System.out::println);

Upvotes: 2

Related Questions