Java Stream API Methods: Terminal and Intermediate Operations - Quiz

Total: 7 questions

1. 

How do you tell a terminal Stream API operation from an intermediate one?

By the return type. If the method returns Stream, IntStream, LongStream or DoubleStream, it is intermediate: it only extends the pipeline and does not execute the lambda you passed in. If it returns a concrete value, a collection, an array, an Optional, an Iterator, a Spliterator or void, it is terminal: it triggers the whole pipeline, produces the result and closes the stream.

Every chain has the same shape: a source, then zero or more intermediate operations, then exactly one terminal operation. Without a terminal operation the code compiles and runs but does nothing at all, because every intermediate operation is lazy.

2. 

What forms does reduce() have and when does it return an Optional?

reduce() is a terminal operation that applies a binary operator to the elements one after another and folds the stream into a single value. The form with an identity value always has a result, so it returns the plain type T. The form without an identity has nothing to return for an empty stream, so its result is wrapped in Optional<T>. The third form also takes a combiner function that merges partial results and is meant for parallel streams.

int sum = Stream.of(1, 2, 3, 4, 5)
        .reduce(0, Integer::sum);              // 15, no Optional needed

Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .reduce(Integer::max);                 // Optional[5]

String sentence = Stream.of("Stream", "API", "reduce")
        .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); // Stream API reduce

For everyday tasks the specialised methods are more convenient: IntStream.sum() instead of reduce(0, Integer::sum) and Collectors.joining(" ") instead of gluing strings by hand. reduce() earns its place where no ready-made collector exists.

3. 

How do you turn a stream into an array, and how do the toArray() variants differ?

toArray() is a terminal operation. With no argument it returns an Object[], because type erasure leaves the runtime without knowledge of the actual element type. To get a typed array, pass an array constructor reference such as String[]::new. On primitive streams toArray() hands back int[], long[] or double[] directly.

Object[] objects = Stream.of("a", "b", "c").toArray();

String[] letters = Stream.of("a", "b", "c").toArray(String[]::new);
System.out.println(Arrays.toString(letters)); // [a, b, c]

int[] numbers = IntStream.rangeClosed(1, 5).toArray();
System.out.println(Arrays.toString(numbers)); // [1, 2, 3, 4, 5]
4. 

Why does a stream have iterator() and spliterator(), and how do they differ?

Both methods are terminal: they close the stream and return an iterator through which elements are pulled manually. That is useful when the result has to go into an older API that expects an Iterator, or when the traversal must stop on a complex condition that is awkward to express with filter() and takeWhile().

A Spliterator (splittable iterator) differs from an ordinary iterator in that it can split itself in half through trySplit() and reports its characteristics (size, sortedness, distinctness). That is exactly the mechanism parallel streams are built on.

Iterator<String> it = Stream.of("sun", "sea", "sand").iterator();
while (it.hasNext()) {
    System.out.println(it.next()); // sun, sea, sand
}

Spliterator<String> sp = Stream.of("sun", "sea", "sand").spliterator();
sp.tryAdvance(s -> System.out.println("first: " + s)); // first: sun
sp.forEachRemaining(System.out::println);              // sea, sand
5. 

What is the difference between forEach() and forEachOrdered()?

In a sequential stream they behave identically. The difference shows up in a parallel stream: forEach() gives no ordering guarantee and processes elements in whatever order the worker threads deliver them, while forEachOrdered() must walk the elements strictly in the encounter order of the source.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8);

nums.parallelStream().forEach(System.out::print);
// for example 56781234 - the order is arbitrary and changes between runs

nums.parallelStream().forEachOrdered(System.out::print);
// always 12345678

forEachOrdered() is not free: it forces a parallel stream to synchronise how results are emitted and often eats the entire benefit of parallelism. When order matters, collecting the result with collect() is usually simpler — collectors preserve the encounter order on their own.

6. 

What is the difference between findFirst() and findAny()?

findFirst() always returns the first element in encounter order. findAny() returns any available element and is therefore cheaper in a parallel stream: it does not have to wait for the task that handles the beginning of the data. In a sequential stream the two usually give the same answer, but the contract does not guarantee it, so you cannot rely on the match.

Both methods are terminal and return Optional<T>, because the stream may turn out to be empty and there would be nothing to return.

7. 

How do stateless operations differ from stateful ones, and why does it matter?

Stateless — the operation handles every element independently and remembers nothing about the previous ones: filter(), map(), flatMap(), peek(). Such operations parallelise perfectly and need no extra memory.

Stateful — the operation must see other elements, sometimes the entire stream, before it can produce a result: sorted(), distinct(), limit(), skip(), takeWhile(), dropWhile(). sorted() and distinct() have to buffer data, while limit() and skip() keep a counter of the elements that have gone through.

The practical takeaway: stateful operations cost more and are best placed after filtering, and on an infinite stream sorted() and distinct() simply hang — they wait for an end of data that never arrives.

Page 1 of 1