Stream API practice questions

From OCP Java SE 21 (1Z0-830) · 19 questions on this topic

Stream API practice questions from OCP Java SE 21 (1Z0-830). This pack has 19 questions tagged Stream API, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Stream API

  1. Question 1

    What does this program print? ```java import java.util.stream.*; public class Main { public static void main(String[] args) { System.out.println(IntStream.of(2, 3, 4, 6).average().getAsDouble()); } } ```

    1. A. OptionalDouble[3.75]

      What would print if the getAsDouble() call were dropped and the wrapper passed to println: it is OptionalDouble's toString. getAsDouble() returns the primitive, so no wrapper text appears.

    2. B. 3

      Assumes average() performs integer division (15 / 4 = 3), by analogy with sum(). The return type OptionalDouble shows the mean is computed in double, with no truncation.

    3. C. 3.0

      The same integer-division belief with the truncated result widened to double on the way out. average() never truncates; the true mean is 3.75.

    4. D. 3.75Correct answer

      IntStream.average() computes the mean in double (15 over 4 = 3.75) and returns OptionalDouble; getAsDouble() unwraps the present value to the primitive 3.75.

    Explanation

    Trace: `IntStream.average()` is declared to return `OptionalDouble` — the arithmetic mean is computed in `double`, not in `int`, even though the source is a primitive int stream. The elements sum to 15 over 4 elements, so the mean is 3.75. `getAsDouble()` unwraps the present `OptionalDouble` to the primitive `double` 3.75, and `println(double)` prints `3.75`. Why the others are wrong: `3` assumes `average()` on an `IntStream` performs integer division (15 / 4 = 3), by analogy with `sum()`, which really does return `int`. The return type `OptionalDouble` is the giveaway that no truncation happens. `3.0` is the same integer-division belief with the truncated result widened to `double` on the way out. `average()` never truncates. `OptionalDouble[3.75]` is what would print if the `getAsDouble()` call were dropped and the wrapper were passed to `println` — it is `OptionalDouble`'s `toString`. The value is right, but `getAsDouble()` returns the primitive, so no wrapper text is printed. Exam tip: on the primitive streams, `sum()` returns the primitive type (`int`/`long`/`double`) and can overflow, while `average()` always returns `OptionalDouble` because an empty stream has no mean. The reverse trap is calling `getAsDouble()` on the average of an empty stream — that throws `NoSuchElementException`.

  2. Question 2

    The three-argument form of groupingBy is used with an explicit map factory. What is printed? ```java import java.util.*; import java.util.stream.*; public class Main { record Item(String category, int qty) {} public static void main(String[] args) { List<Item> cart = List.of( new Item("pen", 5), new Item("book", 2), new Item("mug", 1), new Item("book", 3)); TreeMap<String, Integer> totals = cart.stream() .collect(Collectors.groupingBy(Item::category, TreeMap::new, Collectors.summingInt(Item::qty))); System.out.println(totals + " " + totals.firstKey()); } } ```

    1. A. {book=5, mug=1, pen=5} bookCorrect answer

      The map-factory overload makes the result a real sorted map, so it iterates in key order (book, mug, pen) with first key "book", and the summing collector totals the two book quantities to 5.

    2. B. {book=2, mug=1, pen=1} book

      This is what a counting collector would give: it counts items per category instead of summing their quantities.

    3. C. {pen=5, book=5, mug=1} pen

      Assumes the map preserves encounter order; only an insertion-ordered map factory would do that, whereas a sorted map orders by key, so the first key is "book", not "pen".

    4. D. Compilation fails

      The three-argument grouping is typed to the supplier's map type, so assigning its result to a sorted-map variable is valid and compiles.

    Explanation

    The three-argument grouping collector uses the supplied map factory as its result type, so a sorted-map factory yields a genuinely key-ordered map whose first key follows that ordering rather than encounter order. Its downstream collector determines each value, and a summing collector accumulates a numeric total per group rather than counting occurrences.

  3. Question 3

    Which two statements about stream pipelines are correct? (Choose two.)

    1. A. Intermediate operations run only when a terminal operation is invokedCorrect answer

      Intermediate operations (filter, map, peek, ...) only add a stage to the pipeline description; nothing touches the source until a terminal operation starts pulling elements.

    2. B. A stream can be traversed a second time after its terminal operation completes

      A stream is single-use: operating on one that has already been operated upon throws IllegalStateException; you must rebuild the pipeline from the source.

    3. C. peek is an intermediate operation whose Javadoc says it exists mainly to support debuggingCorrect answer

      peek is intermediate: it forwards each element unchanged after invoking its action, and its Javadoc states it exists mainly to support debugging.

    4. D. map transforms all elements eagerly at the moment it is called

      map is as lazy as every other intermediate operation; calling it performs no per-element work by itself.

    Explanation

    Every intermediate operation is lazy — it only records a stage in the pipeline and does no per-element work until a terminal operation begins pulling elements — and a stream is single-use, throwing IllegalStateException if you try to operate on it twice. Before computing any output, classify each named operation as intermediate or terminal. The two standing traps are reusing a consumed stream and assuming intermediate operations execute eagerly, or at all when no terminal operation is present.

  4. Question 4

    Which two statements about the collectors in `java.util.stream.Collectors` are correct? (Choose two.)

    1. A. `Collectors.groupingBy(classifier)` returns a map whose keys are arranged in the natural order of the classifier's result

      Assumes single-argument groupingBy is sorted. It is specified to use an unspecified map (in practice a HashMap), so keys are unordered; pass a map factory like TreeMap::new for sorted keys.

    2. B. `Collectors.toMap(keyFn, valueFn)` with no merge function throws IllegalStateException at run time if two elements produce the same keyCorrect answer

      The two-argument toMap refuses to overwrite an existing key, throwing IllegalStateException("Duplicate key ...") when two elements produce the same key; supplying a third merge-function argument is the fix.

    3. C. `Collectors.counting()` supplies an Integer count for each group

      Confuses it with List.size(). counting() is declared Collector<T,?,Long>, so the count's runtime type is Long — which is why Map<Integer,Long>, not Map<Integer,Integer>, is what compiles.

    4. D. `Collectors.partitioningBy(predicate)` returns a map that contains both the true and the false key even when one side matched no elementsCorrect answer

      partitioningBy is defined over Boolean, so the result always has size 2; get(true) or get(false) returns an empty list, never null — unlike groupingBy, which creates a key only when at least one element lands there.

    Explanation

    `Collectors.toMap(keyFn, valueFn)` with no merge function throws IllegalStateException at run time if two elements produce the same key: the two-argument `toMap` accumulates through `uniqKeysMapAccumulator`, which refuses to overwrite a key that is already present. Collecting `Stream.of("aa", "bb")` with `toMap(String::length, s -> s)` gives both elements the key `2` and throws `java.lang.IllegalStateException: Duplicate key 2 (attempted merging values aa and bb)`. Supplying a third, merge-function argument is the fix. `Collectors.partitioningBy(predicate)` returns a map that contains both the true and the false key even when one side matched no elements: partitioning is defined over `Boolean`, so the result always has size 2. `Stream.of(1, 3, 5).collect(partitioningBy(n -> n % 2 == 0))` yields `{false=[1, 3, 5], true=[]}` — `get(true)` returns an empty list, never `null`. This is exactly where it differs from `groupingBy`, which creates a key only when at least one element lands there. Why the others are wrong: `Collectors.groupingBy(classifier)` returns a map whose keys are arranged in the natural order... assumes the default `groupingBy` is sorted. The single-argument form is specified to use an unspecified map — in practice a `HashMap`. Collecting `Stream.of("pear", "apple", "fig")` grouped by identity produced a `java.util.HashMap` whose key set printed as `[apple, pear, fig]`, which is not natural order (`fig` would sit between them). To get sorted keys you must pass a map factory, e.g. `groupingBy(fn, TreeMap::new, toList())`. `Collectors.counting()` supplies an Integer count... confuses it with `List.size()`. `counting()` is declared as `Collector<T, ?, Long>`; the collected value's runtime class is `java.lang.Long`, which is why `Map<Integer, Long>` — not `Map<Integer, Integer>` — is the type that compiles. Exam tip: `partitioningBy` always yields exactly two entries; `groupingBy` yields one entry per *observed* key and nothing for keys no element produced. And any count coming out of a collector is a `long`, while `Collectors.summingInt` gives an `Integer`.

  5. Question 5

    **`Collectors.partitioningBy`** always produces a `Map` with exactly two entries — one for each boolean value — even when one partition receives no elements. **`Collectors.groupingBy`** makes no such guarantee and omits keys for which no element is classified. What does the program below print? ```java import java.util.*; import java.util.stream.*; public class Main { public static void main(String[] args) { List<Integer> numbers = List.of(2, 4, 6, 8); Map<Boolean, List<Integer>> result = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 != 0)); System.out.println(result.size() + " " + result.get(false).size()); } } ```

    1. A. 1 4

      Assumes that a partition receiving no elements has its key omitted from the map, as Collectors.groupingBy would do. Collectors.partitioningBy is specified to always produce entries for both true and false, so result.size() is always 2 regardless of how many elements fall into each partition.

    2. B. 2 0

      Confuses which partition receives the even numbers. The predicate is 'is odd' (n % 2 != 0), so all four even numbers evaluate to false and land in the false partition, giving it size 4. The true partition (odd numbers) receives no elements and has size 0 — it is the true partition that is empty, not the false one.

    3. C. 2 4Correct answer

      The predicate (n % 2 != 0) tests for odd numbers. All four inputs are even, so no element satisfies the predicate; the true partition holds an empty list and the false partition holds [2, 4, 6, 8]. Collectors.partitioningBy guarantees both Boolean keys are always present, so result.size() is 2 and result.get(false).size() is 4.

    4. D. Throws NullPointerException

      result.get(false) does not return null. Collectors.partitioningBy guarantees that both the true and false keys are always present in the result map, each mapping to a (possibly empty) List. Calling .size() on the returned list is therefore safe, and no NullPointerException is thrown.

    Explanation

    Collectors.partitioningBy guarantees exactly two map entries — keyed by true and false — regardless of how many elements fall into each group. The predicate tests for odd numbers (n % 2 != 0), and since all four input values are even, the true partition receives an empty list and the false partition receives all four elements. This two-key guarantee is precisely what distinguishes partitioningBy from groupingBy, which omits a key entirely when no element is classified under it. Calling get(false) on the resulting map returns a non-null List of four elements, and the map reports size two.

  6. Question 6

    What does this print? ```java import java.util.stream.*; public class Main { public static void main(String[] args) { String s = Stream.iterate(1, n -> n * 2) .limit(4) .map(String::valueOf) .collect(Collectors.joining(",")); System.out.print(s); } } ```

    1. A. 1,2,4,8Correct answer

      iterate emits the seed 1 first, then 2, 4, 8, ...; limit(4) keeps the first four elements, which map to strings and join to 1,2,4,8.

    2. B. 2,4,8,16

      Forgets that iterate emits the seed itself before applying the function, so it drops the leading 1 and shifts the sequence forward.

    3. C. 1,2,4,8,16

      Five elements; limit(4) keeps exactly four, so 16 is never produced.

    4. D. The program never terminates

      An infinite source is legal as long as a short-circuiting operation like limit bounds the pipeline; only a fully-consuming terminal op on the unbounded stream would hang.

    Explanation

    `Stream.iterate(seed, f)` emits the raw seed as its first element and only then applies the function repeatedly, so the seed value appears in the output. The source is infinite but lazy, and a short-circuiting operation like limit(n) truncates it to exactly n elements, so the pipeline terminates fine. 'Never terminates' is only correct when NO short-circuiting operation sits between the infinite source and a fully-consuming terminal operation.

  7. Question 7

    The three-argument **`Collectors.groupingBy`** overload accepts a `Supplier<M>` as its second argument, controlling both the `Map` implementation and — when that implementation is ordered — the iteration order of the result. What does the program below print? ```java import java.util.*; import java.util.stream.*; public class Main { public static void main(String[] args) { Map<Integer, String> result = Stream.of("a", "bb", "ccc", "dd", "e") .collect(Collectors.groupingBy( String::length, TreeMap::new, Collectors.joining() )); System.out.println(result); } } ```

    1. A. {1=[a, e], 2=[bb, dd], 3=[ccc]}

      This is the output of the two-argument groupingBy(classifier, mapFactory), which uses Collectors.toList() as the implicit downstream collector. The three-argument form here explicitly passes Collectors.joining(), which concatenates each group's strings into a single String rather than collecting them into a List.

    2. B. {3=ccc, 2=bbdd, 1=ae}

      TreeMap's natural ordering for Integer is ascending (1, 2, 3), not descending. Descending order requires constructing the TreeMap with a reverse comparator — for example, new TreeMap<>(Comparator.reverseOrder()) — not TreeMap::new, which calls the no-arg constructor and uses natural order.

    3. C. {1=a e, 2=bb dd, 3=ccc}

      Collectors.joining() with no arguments uses an empty string as the delimiter; no spaces appear between joined elements. A space delimiter requires Collectors.joining(" "). The no-arg, one-arg, and three-arg overloads of joining() differ precisely in the delimiter, prefix, and suffix they apply.

    4. D. {1=ae, 2=bbdd, 3=ccc}Correct answer

      TreeMap stores keys in natural ascending order for Integer. The length-1 group receives 'a' then 'e' in stream encounter order; Collectors.joining() with no arguments uses an empty delimiter, concatenating them to 'ae'. The length-2 group receives 'bb' then 'dd', giving 'bbdd'. The length-3 group receives only 'ccc'. The TreeMap's toString() prints entries in ascending key order: {1=ae, 2=bbdd, 3=ccc}.

    Explanation

    The three-argument Collectors.groupingBy(classifier, mapFactory, downstream) uses the supplied Supplier to create the accumulation map; TreeMap::new produces a TreeMap whose Integer keys are ordered by natural ascending order. Within each group the downstream Collectors.joining() concatenates strings in stream encounter order with an empty delimiter — the no-argument overload of joining() uses an empty string, not a space or comma. Elements of length one appear in stream order ('a' before 'e'), joining to 'ae'; elements of length two join to 'bbdd'; the single length-three element stays 'ccc'. The TreeMap's toString() prints entries in ascending key order.

  8. Question 8

    What is the output of the following program? ```java import java.util.stream.Stream; public class Main { static int calls = 0; public static void main(String[] args) { long result = Stream.of(5, 3, 1, 4, 2) .filter(n -> { calls++; return n > 2; }) .sorted() .limit(2) .count(); System.out.println(calls + " " + result); } } ```

    1. A. 5 2Correct answer

      `sorted()` is a stateful intermediate operation that buffers all upstream elements before emitting any, forcing the filter predicate to be invoked once for each of the five source elements (`calls == 5`). Three elements satisfy `n > 2`: in source encounter order, 5, 3, and 4. Natural-order sorting produces 3, 4, 5. `limit(2)` retains only 3 and 4, and `count()` returns 2.

    2. B. 2 2

      `limit(2)` is positioned downstream of `sorted()` and cannot cancel the upstream filter stage. `sorted()` is a stateful intermediate operation that must buffer every element it receives before emitting any element, so the filter predicate is still invoked for all five source elements, not just two.

    3. C. 3 2

      `calls++` executes unconditionally every time the predicate is entered — whether the element passes or fails the test. All five elements are tested, so `calls` reaches 5, not 3 (which is the count of elements that happen to satisfy `n > 2`).

    4. D. 5 3

      `limit(2)` is not inert. After `sorted()` emits 3, 4, 5, `limit(2)` passes only 3 and 4 downstream to `count()`, which therefore returns 2, not 3. The value 3 is the count of elements that survive `filter()` before `limit` is applied.

    Explanation

    A stateful intermediate operation such as `sorted()` cannot emit any element until it has consumed all elements from its upstream stage; the java.util.stream package summary describes such operations as ones that 'may need to process the entire input before producing a result.' Because `sorted()` sits between `filter()` and `limit()`, the downstream `limit(2)` has no ability to short-circuit `filter()`: every source element must pass through the predicate, incrementing `calls` to 5. Three of the five integers satisfy `n > 2` (5, 3, and 4 in source order). Sorting them yields 3, 4, 5; `limit(2)` retains 3 and 4; `count()` returns 2. This contrasts with a pipeline that has no stateful stage, where a short-circuit terminal like `findFirst()` can suppress most upstream filter invocations.

Practise all 19 Stream API questions

OCP Java SE 21 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 21

Other topics in this pack