Stream API practice questions

From OCP Java SE 17 (1Z0-829) · 19 questions on this topic

Stream API practice questions from OCP Java SE 17 (1Z0-829). 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

    Stream.iterate is called with three arguments. What does this program print?

    import java.util.stream.*;
    
    public class Main {
        public static void main(String[] args) {
            String s = Stream.iterate(1, i -> i < 20, i -> i * 3)
                    .map(String::valueOf)
                    .collect(Collectors.joining("-"));
            System.out.println(s);
        }
    }
    1. A. 3-9

      Wrong: this assumes the seed is not part of the stream, as if next were applied once before the first emission. The seed is always the first candidate element, so 1 is emitted.

    2. B. 1-3-9Correct answer

      Correct: the three-argument iterate tests the predicate before each emission including the seed, so 1, 3, 9 pass i < 20 and 27 fails; joined with dashes they give 1-3-9.

    3. C. The program never terminates because Stream.iterate produces an infinite stream

      Wrong: this describes the two-argument iterate(seed, next), which is infinite and needs a short-circuiting op. The three-argument overload is bounded by its predicate and is finite.

    4. D. 1-3-9-27

      Wrong: this treats the predicate as a do-while condition - emit first, test afterwards - so 27 slips out before the check fails. The predicate is a hasNext, checked before emission.

    Explanation

    Trace: the three-argument Stream.iterate(seed, hasNext, next) is the stream analogue of a for loop — the predicate is tested before each element is emitted, including the seed. So it tests 1 (< 20, emit), applies next to get 3 (< 20, emit), then 9 (< 20, emit), then 27, which fails i < 20, so the stream ends without emitting it. Three elements survive, map stringifies them and joining("-") glues them with a single dash between neighbours: 1-3-9.

    Why the others are wrong: 1-3-9-27 treats the predicate as a do-while condition — emit first, test afterwards — so 27 slips out before the check fails. The predicate is a hasNext, checked before emission. 3-9 assumes the seed is not part of the stream, as if next were applied once before the first emission. The seed is always the first candidate element. The program never terminates because Stream.iterate produces an infinite stream describes the two-argument iterate(seed, next), which is indeed infinite and needs a short-circuiting op such as limit. The three-argument overload (Java 9) is bounded by its predicate and is a finite stream.

    Exam tip: Stream.iterate(1, i -> i < 20, i -> i * 3) maps one-to-one onto for (int i = 1; i < 20; i *= 3) — seed, condition, update, condition checked first. Reverse trap: with only two arguments the stream is infinite, and a terminal op like collect or count on it hangs forever.

  2. Question 2

    A list of lists is flattened and each element is doubled. What does this program print?

    import java.util.*;
    
    public class Main {
        public static void main(String[] args) {
            List<List<Integer>> nested = List.of(List.of(1, 2), List.of(), List.of(3, 4, 5));
            List<Integer> flat = nested.stream()
                    .flatMap(List::stream)
                    .map(n -> n * 2)
                    .toList();
            System.out.println(flat);
        }
    }
    1. A. The code does not compile because List::stream cannot be passed to flatMap

      Assumes flatMap needs a factory like Stream::of; its parameter is a Function returning a Stream, and List::stream is exactly such a function, so it compiles.

    2. B. [[2, 4], [], [6, 8, 10]]

      This is what map(list -> ...) gives, assuming flatMap preserves nesting; flatMap flattens a stream of streams into a stream of elements.

    3. C. [2, 4]

      Assumes the empty List.of() terminates flattening; an empty inner stream simply contributes zero elements and traversal continues.

    4. D. [2, 4, 6, 8, 10]Correct answer

      flatMap splices the contents of each inner stream into one flat stream (1..5, the empty list adding nothing), then map doubles each, giving [2, 4, 6, 8, 10].

    Explanation

    Trace: flatMap maps each element to a stream and then splices the contents of that stream into the pipeline, so the three inner streams (1, 2), (nothing) and (3, 4, 5) are concatenated into a single flat stream of 1, 2, 3, 4, 5. The empty inner list simply contributes zero elements — it is not a terminator. map(n -> n * 2) then doubles each of the five values and toList() collects them in encounter order, so List.toString prints [2, 4, 6, 8, 10].

    Why the others are wrong: [[2, 4], [], [6, 8, 10]] is what map(list -> ...) would give — it assumes flatMap preserves the nesting and merely transforms each inner list. Flattening is the whole point of flatMap; a stream of streams comes back as a stream of elements. [2, 4] assumes the empty List.of() ends the flattening, as if an empty inner stream short-circuits the outer one. An inner stream that yields no elements is skipped and traversal continues with the next element. The code does not compile because List::stream cannot be passed to flatMap encodes the belief that flatMap needs a factory such as Stream::of. Its parameter is Function<? super T, ? extends Stream<? extends R>>, and List::stream is exactly such a function.

    Exam tip: map is 1-to-1, flatMap is 1-to-many (including 1-to-zero). Whenever a lambda would return a Stream, Collection or Optional and you want its contents rather than the container itself, flatMap is the operation. Reverse trap: using flatMap where the mapper returns a plain value does not compile, because a value is not a Stream.

  3. Question 3

    What is the output?

    import java.util.*;
    import java.util.stream.*;
    public class Main {
        public static void main(String[] args) {
            Optional<Integer> r = Stream.of(1,2,3,4)
                .filter(n -> n % 2 == 0)
                .reduce((a,b) -> a + b);
            System.out.println(r.get());
        }
    }
    1. A. 10

      This is the sum of all four elements; the filter removed the odd values before the reduction, so they must not be included.

    2. B. 2

      Assumes the reduction returns only the first surviving element, but reduce accumulates every element rather than picking one.

    3. C. 4

      This is merely the last surviving element; reduce combines the elements, it does not select a single one.

    4. D. 6Correct answer

      Correct: the survivors 2 and 4 are combined left to right by the accumulator to give 6, wrapped in an Optional that get() then unwraps.

    Explanation

    The filter keeps only the even values, and the single-argument reduce with no identity folds those survivors together left to right, producing their combined sum wrapped in an Optional because the stream could have been empty. Unwrapping the Optional prints that total. The one-argument reduce returns Optional precisely because it has no seed value to fall back on.

  4. Question 4

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

    1. A. In groupingBy(String::length, Collectors.counting()) the value type of the resulting map is LongCorrect answer

      Collectors.counting() is declared Collector<T,?,Long>, so using it as the downstream of groupingBy produces a Map whose value type is Long.

    2. B. Collectors.partitioningBy returns a map containing both the false and true keys even when one partition matches no elementsCorrect answer

      partitioningBy always splits into exactly two buckets, so both the false and true keys are present even when one partition is empty (its value being an empty list, not a missing key).

    3. C. Collectors.groupingBy(classifier) returns a map whose keys are iterated in ascending natural order

      Confuses grouping with sorting; groupingBy returns an unordered HashMap with no ordering guarantee, so keys are not iterated in ascending natural order (a TreeMap factory is needed for that).

    4. D. The List returned by Collectors.toList() is unmodifiable, so calling add on it throws UnsupportedOperationException

      Swaps the two toLists; Collectors.toList() returns a mutable ArrayList that accepts add, whereas it is Stream.toList() that returns an unmodifiable list throwing UnsupportedOperationException.

    Explanation

    In groupingBy(String::length, Collectors.counting()) the value type of the resulting map is Long is correct: Collectors.counting() is declared as Collector<T, ?, Long>, so a groupingBy that uses it as its downstream produces a Map<K, Long>. Grouping "a", "bb", "cc" by length and reading the bucket for key 2 yields a value whose runtime class is java.lang.Long (value 2) — a common trip-up when the map is declared as Map<Integer, Integer>, which does not compile.

    Collectors.partitioningBy returns a map containing both the false and true keys ... is correct: partitioning is defined to split into exactly two buckets, and both are always present. Partitioning 1, 3, 5 on n % 2 == 0 gives {false=[1, 3, 5], true=[]} — a map of size 2 whose true entry is an empty list, not a missing key. That is precisely why partitioningBy is safe to get(true) on without a null check, while groupingBy is not.

    Why the others are wrong: Collectors.groupingBy(classifier) returns a map whose keys are iterated in ascending natural order confuses grouping with sorting. The Javadoc states there are no guarantees on the type, mutability, serializability or thread-safety of the returned map; in practice it is a HashMap. Grouping "zebra", "apple", "mango" by first character iterates its keys as [a, z, m] — not ascending. Pass a map factory such as TreeMap::new (the three-argument groupingBy) if you need sorted keys. The List returned by Collectors.toList() is unmodifiable ... swaps the two "toList"s. Collectors.toList() gives back a mutable java.util.ArrayList, and add on it succeeds. It is Stream.toList() (added in Java 16) that returns an unmodifiable list and throws UnsupportedOperationException on add. Note the Javadoc guarantees no more than that for Collectors.toList() — do not rely on mutability either; use Collectors.toCollection(ArrayList::new) when you need it.

    Exam tip: know the shape of each collector's result — counting() is Long, summingInt is Integer, averagingInt is Double, partitioningBy always has both boolean keys, and groupingBy gives an unordered map. Reverse trap: Stream.toList() and Collectors.toList() look interchangeable but differ in mutability.

  5. Question 5

    Words are collected into a map of word to length. What does this program print?

    import java.util.*;
    import java.util.stream.*;
    
    public class Main {
        public static void main(String[] args) {
            Map<String, Integer> lengths = Stream.of("kiwi", "fig", "banana")
                    .collect(Collectors.toMap(s -> s, String::length, (a, b) -> a, TreeMap::new));
            System.out.println(lengths);
        }
    }
    1. A. {banana=6, fig=3, kiwi=4}Correct answer

      Correct: the TreeMap::new supplier makes the four-argument toMap build a TreeMap, so entries iterate in ascending natural (alphabetical) key order regardless of the stream's encounter order.

    2. B. {fig=3, kiwi=4, banana=6}

      Wrong: this sorts by the value (3, 4, 6) rather than by the key. A TreeMap orders by key; sorting on values would need an explicit comparator over the entries.

    3. C. {kiwi=4, fig=3, banana=6}

      Wrong: this assumes the map preserves stream encounter order, ignoring the TreeMap::new supplier. Only a LinkedHashMap supplier would give encounter order.

    4. D. The code does not compile because toMap does not accept a map supplier

      Wrong: this encodes the belief that toMap has only two- and three-argument forms. There is a four-argument overload whose last parameter is a Supplier for the result map, which is exactly what TreeMap::new supplies here.

    Explanation

    Trace: the four-argument toMap takes a key mapper, a value mapper, a merge function and a map supplier. Each word becomes a key, its length becomes the value, and the supplier TreeMap::new decides the container — so the entries live in a TreeMap and iterate in ascending natural (alphabetical) key order regardless of the stream's encounter order. TreeMap.toString therefore prints {banana=6, fig=3, kiwi=4}. The merge function (a, b) -> a is never called here because the three words are distinct keys.

    Why the others are wrong: {kiwi=4, fig=3, banana=6} assumes the map preserves stream encounter order, ignoring the TreeMap::new supplier. Only a LinkedHashMap supplier would give that. {fig=3, kiwi=4, banana=6} sorts by the value (3, 4, 6) rather than the key. A TreeMap orders by key; sorting on values needs an explicit comparator over the entries. The code does not compile because toMap does not accept a map supplier encodes the belief that toMap only has two- and three-argument forms. There is a four-argument overload whose last parameter is a Supplier<M> for the result map — that is the only way to choose the map type.

    Exam tip: a toMap or groupingBy result is a HashMap with no order guarantee unless you supply a map factory. TreeMap::new gives sorted keys, LinkedHashMap::new gives encounter order. Reverse trap: the merge function is not optional decoration — drop it from a stream with duplicate keys and toMap throws IllegalStateException.

  6. Question 6

    What is printed (map toString order aside, focus on the values)?

    import java.util.*;
    import java.util.stream.*;
    public class Main {
        public static void main(String[] args) {
            Map<Integer,Long> m = Stream.of("a","bb","cc","ddd")
                .collect(Collectors.groupingBy(String::length, Collectors.counting()));
            System.out.println(m.get(2));
        }
    }
    1. A. 2Correct answer

      Correct: grouping by length with a counting downstream gives {1=1, 2=2, 3=1}, and the length-two group contains "bb" and "cc", so its count is 2.

    2. B. 1

      This is the count for the length-one or length-three groups; two strings share length two, so its count is not one.

    3. C. 3

      No length occurs three times in this data, so a count of three matches nothing here.

    4. D. [bb, cc]

      This would be the value only if the default toList() downstream were used; counting() replaces the list with a Long count.

    Explanation

    A downstream counting collector makes groupingBy map each distinct key to the number of elements in that group rather than to a list, so the value type becomes Long. Two of the strings share length two, so the count stored under that key is two. The downstream collector replaces the default toList(), which is why the value is a count and not a list of strings.

  7. Question 7

    What is the output?

    import java.util.stream.*;
    public class Main {
        public static void main(String[] args) {
            int sum = IntStream.rangeClosed(1, 5).sum();
            System.out.println(sum);
        }
    }
    1. A. 10

      This is what range(1, 5) would give: the half-open form excludes the upper bound, summing only 1+2+3+4.

    2. B. 14

      Matches no variant of the range; it would require dropping the 1 from the closed range.

    3. C. 20

      Corresponds to no range here, such as 2..6; both bounds are exactly as written.

    4. D. 15Correct answer

      Correct: rangeClosed(1, 5) is inclusive of 5, so 1+2+3+4+5 = 15.

    Explanation

    rangeClosed includes its upper bound, so it produces one through five, and summing them gives fifteen. The half-open range form would exclude the upper bound and produce a smaller total. The exam hides this off-by-one inside a sum so that a wrong bound lands on one of the distractors.

  8. Question 8

    The three-argument groupingBy below totals order quantities per category. What does this program print?

    import java.util.*;
    import java.util.stream.*;
    
    public class Main {
        record Item(String cat, int qty) {}
    
        public static void main(String[] args) {
            List<Item> items = List.of(
                    new Item("fruit", 3), new Item("veg", 5), new Item("fruit", 4),
                    new Item("dairy", 2), new Item("veg", 1));
            Map<String, Integer> totals = items.stream()
                    .collect(Collectors.groupingBy(Item::cat, TreeMap::new,
                            Collectors.summingInt(Item::qty)));
            System.out.println(totals);
        }
    }
    1. A. {fruit=7, veg=6, dairy=2}

      Has the right totals but encounter-order keys, which is what an insertion-ordered map factory would give; the supplied sorted-map factory orders keys alphabetically.

    2. B. {dairy=1, fruit=2, veg=2}

      Counts the items per group instead of summing their quantities, which is counting rather than summing behaviour.

    3. C. {dairy=2, fruit=4, veg=1}

      Keeps only the last item's quantity per key (a map-merge behaviour); the summing collector adds the quantities of all items in each group.

    4. D. {dairy=2, fruit=7, veg=6}Correct answer

      Correct — the sorted-map factory orders keys alphabetically (dairy, fruit, veg) and the summing collector totals each group's quantities (fruit 3+4=7, veg 5+1=6, dairy 2).

    Explanation

    The three-argument grouping collector builds its result with the supplied map factory, and a sorted-map factory arranges the keys in natural (alphabetical) order rather than encounter order. Its downstream summing collector adds up each group's integer quantities, as opposed to merely counting the elements or keeping only the last one per key.

Practice all 19 Stream API questions

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

Drill 20 questions on Stream API →Open OCP Java SE 17 →

Other topics in this pack