Reputation: 13
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
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 thehasNext
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).
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.
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