Generics and Collections practice questions

From OCP Java SE 25 (1Z0-831) · 20 questions on this topic

Generics and Collections practice questions from OCP Java SE 25 (1Z0-831). This pack has 20 questions tagged Generics and Collections, 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 Generics and Collections

  1. Question 1

    What does this print? ```java import java.util.*; public class Main { public static void main(String[] args) { TreeMap<Integer,String> m = new TreeMap<>(); m.put(10, "a"); m.put(20, "b"); m.put(30, "c"); m.put(40, "d"); System.out.println(m.headMap(30) + " " + m.tailMap(30)); } } ```

    1. A. {10=a, 20=b, 30=c} {40=d}

      This treats headMap as inclusive of the bound 30. The single-argument headMap form is exclusive at the high end, so 30 belongs to the tail view instead.

    2. B. {10=a, 20=b} {30=c, 40=d}Correct answer

      The single-argument headMap(30) returns keys strictly less than 30, and tailMap(30) returns keys greater than or equal to 30, so the two views partition the map exactly.

    3. C. {10=a, 20=b} {40=d}

      This drops the 30 entry entirely, as if both views excluded the bound. tailMap(30) includes 30, so it is never lost.

    4. D. {30=c, 40=d} {10=a, 20=b}

      This swaps the two views. headMap returns the low keys and tailMap the high keys, not the other way around.

    Explanation

    A `TreeMap` keeps keys sorted in ascending order. The single-argument `headMap(k)` is exclusive of k while `tailMap(k)` is inclusive of k, so together they partition the whole map with no overlap and no gap. The two-argument `headMap(k, true)` / `tailMap(k, false)` overloads let you flip either boundary's inclusivity.

  2. Question 2

    What does this print? ```java import java.util.*; public class Main { record P(String name, int age) {} public static void main(String[] args) { List<P> l = new ArrayList<>(List.of( new P("ann", 30), new P("bob", 20), new P("ann", 20))); l.sort(Comparator.comparing(P::name).thenComparing(P::age).reversed()); StringBuilder sb = new StringBuilder(); for (P p : l) sb.append(p.name()).append(p.age()).append(" "); System.out.println(sb.toString().trim()); } } ```

    1. A. bob20 ann30 ann20Correct answer

      comparing(name).thenComparing(age) sorts by name then age ascending, and .reversed() wraps and inverts that entire composed comparator, so the whole ordering flips to name descending then age descending: bob20, ann30, ann20.

    2. B. ann20 ann30 bob20

      This is the ordering without the final reversed() call. It ignores that reversed() flips the entire comparator.

    3. C. ann30 ann20 bob20

      This applies reversed() only to the last key, age, while keeping name ascending. But reversed() reverses everything to its left, not just the final thenComparing key.

    4. D. bob20 ann20 ann30

      This reverses only the name key and leaves age ascending. reversed() is not selective; it negates the combined comparator as a whole.

    Explanation

    `.reversed()` reverses the entire comparator chain assembled up to that point, not just the most recent `thenComparing` key. So a comparator built by name-then-age ascending becomes name-then-age descending as a single unit. To reverse only one key, negate that key locally, for example `thenComparing(Comparator.comparingInt(P::age).reversed())`.

  3. Question 3

    Two ArrayLists with different type arguments have their runtime classes compared. What does this print? ```java import java.util.*; public class Main { public static void main(String[] args) { List<String> words = new ArrayList<>(); List<Integer> nums = new ArrayList<>(); System.out.println(words.getClass() == nums.getClass()); } } ```

    1. A. Compilation fails: incomparable types for the == comparison

      Believes the compiler blocks the comparison because List<String> and List<Integer> are unrelated; getClass() is declared to return Class<?> and those two references are cast-compatible, so == is legal.

    2. B. true only while both lists are empty; once elements are added the classes differ

      Believes the runtime class somehow tracks the elements the list holds; a collection's class is fixed at construction and its contents are irrelevant to getClass().

    3. C. false

      Believes generics are reified — that ArrayList<String> and ArrayList<Integer> are separate runtime classes; Java erases the type argument, which is precisely why generic array creation and parameterized instanceof are banned.

    4. D. trueCorrect answer

      Generics are compile-time only; after erasure both objects are plain java.util.ArrayList instances, so there is exactly one ArrayList Class object and getClass() returns the same reference for both, making == true.

    Explanation

    Trace: generics are compile-time only. After erasure both objects are plain `java.util.ArrayList` instances — the type argument is not stored in the object, so there is exactly one ArrayList Class object in the JVM and getClass() returns the same reference for both. The == on those two references is true, and the program prints `true`. Why the others are wrong: `false` encodes the belief that generics are reified — that ArrayList<String> and ArrayList<Integer> are separate runtime classes the way they would be in C# or C++. Java erases them; that is precisely why you cannot write `new T[]`, `x instanceof List<String>`, or two overloads that differ only in their type argument. `Compilation fails: incomparable types for the == comparison` encodes the belief that the compiler blocks the comparison because List<String> and List<Integer> are unrelated. getClass() is declared to return Class<?> (a capture of the erased type), and those two Class references are cast-compatible, so == is legal. `true only while both lists are empty; once elements are added the classes differ` encodes the belief that the runtime class somehow tracks the elements the list holds. A collection's class is fixed at construction; its contents are irrelevant to getClass(). Exam tip: "the type argument does not exist at run time" answers a whole family of exam questions. Anything that would need the type argument at run time is either banned by the compiler (generic array creation, instanceof against a parameterized type, catching a type parameter) or is simply a no-op (getClass, a cast to List<String>).

  4. Question 4

    What is the result? ```java import java.util.*; public class Main { public static void main(String[] args) { List<Integer> l = new ArrayList<>(List.of(1, 2, 3, 4)); for (Integer n : l) { if (n == 1) l.remove(n); } System.out.println(l); } } ```

    1. A. [2, 3, 4]

      This assumes the removal completes cleanly. The structural change made directly on the list, outside the iterator, poisons the very next next() call so the loop cannot finish.

    2. B. [1, 2, 3, 4]

      This assumes remove did nothing. It does remove the element before the iterator detects the concurrent modification, and the detection aborts the program with an exception rather than printing.

    3. C. Compilation fails: cannot modify a list while iterating

      The code compiles fine. The fail-fast check is a runtime behaviour of the iterator, not a compile-time rule the language enforces.

    4. D. Throws ConcurrentModificationExceptionCorrect answer

      The enhanced for-loop iterates via the list's Iterator. Calling remove directly on the list bumps its modCount without going through the iterator, so the next next() call's checkForComodification sees modCount != expectedModCount and throws ConcurrentModificationException.

    Explanation

    An enhanced for-loop iterates through the collection's `Iterator`, which records the expected `modCount` when it is created. Structurally modifying the list directly changes `modCount` behind the iterator's back, so the fail-fast check on the next `next()` call throws ConcurrentModificationException at runtime. Safe deletion during iteration uses `Iterator.remove()`, which keeps the two counts in sync, or `Collection.removeIf(...)`.

  5. Question 5

    A permission set is built with the Set.of factory. What is the result of compiling and running this program? ```java import java.util.*; public class Main { public static void main(String[] args) { Set<String> perms = Set.of("read", "write", "read"); System.out.println(perms.size()); } } ```

    1. A. Throws UnsupportedOperationException

      The reflex answer for anything immutable, but UnsupportedOperationException comes from a mutation attempt on an already-built collection; nothing here mutates — the failure happens during construction.

    2. B. Prints 3

      Assumes Set.of is a fixed-size holder that keeps whatever you hand it, ignoring the Set contract; the factory rejects the duplicate instead of storing three elements.

    3. C. Throws IllegalArgumentExceptionCorrect answer

      Correct: Set.of is specified to throw IllegalArgumentException when any two elements are equal, so the duplicate "read" makes construction fail and the set is never built.

    4. D. Prints 2

      Assumes the factory silently de-duplicates the way new HashSet<>(List.of(...)) does; the of() factories deliberately treat a duplicate as a caller bug and throw.

    Explanation

    Trace: the code compiles — Set.of(E...) accepts any three strings, so duplicates are not a compile-time problem. At run time the factory itself rejects them: Set.of is specified to throw IllegalArgumentException when any two elements are equal, and the run ends with `java.lang.IllegalArgumentException: duplicate element: read` thrown from ImmutableCollections$SetN.<init>. That is why `Throws IllegalArgumentException` is correct — the set is never constructed at all. Why the others are wrong: `Prints 2` encodes the belief that the factory silently de-duplicates the way `new HashSet<>(List.of(...))` does. The copy constructor does absorb duplicates; the of() factories deliberately do not — they treat a duplicate as a caller bug. `Prints 3` encodes the belief that Set.of is just a fixed-size list-like holder that keeps whatever you hand it, ignoring the Set contract. `Throws UnsupportedOperationException` is the reflex answer for anything involving an immutable collection, but that exception is thrown by a *mutation* attempt (put/add/remove) on an already-built immutable collection. Nothing here mutates: the failure happens during construction. Exam tip: with the Java 9+ factories, remember which failures are construction-time and which are mutation-time. Construction-time: a duplicate element in Set.of, a duplicate key in Map.of (both IllegalArgumentException), and a null element/key/value (NullPointerException). Mutation-time: add/remove/put/sort/clear on the result (UnsupportedOperationException). The reverse trap is Map.of("a", 1, "b", 1) — duplicate *values* are fine; only duplicate keys blow up.

  6. Question 6

    What does this print? ```java import java.util.*; public class Main { public static void main(String[] args) { List<Integer> l = new ArrayList<>(List.of(5, 10, 15)); l.remove(1); System.out.println(l); } } ```

    1. A. [10, 15]

      This removes index 0, the first element. The argument is index 1, not the first position, so this misidentifies which index was targeted.

    2. B. [5, 15]Correct answer

      The int literal 1 binds to the remove(int index) overload, which removes the element at index 1 (the value 10), leaving [5, 15].

    3. C. [5, 10, 15]

      This assumes remove(Object) searches for the value 1 (not present, so no change). But an int argument always selects the index overload, never the object overload.

    4. D. [5, 10]

      This removes the last element, which would require index 2. The argument selects index 1, not the final position.

    Explanation

    `List` declares two overloads, `remove(int index)` and `remove(Object o)`. A bare `int` literal binds to the index overload, so the element at that position is deleted rather than a matching value being searched for. To remove by VALUE from a `List<Integer>` you must force the object overload with `l.remove(Integer.valueOf(...))`. This overload trap is a favourite on the exam.

  7. Question 7

    A list containing null elements is sorted with a null-tolerant comparator. What does this print? ```java import java.util.*; public class Main { public static void main(String[] args) { List<String> names = new ArrayList<>(Arrays.asList("bob", null, "amy", null, "cara")); names.sort(Comparator.nullsLast(Comparator.comparing(String::length))); System.out.println(names); } } ```

    1. A. Throws NullPointerException

      That is what happens without the nullsLast wrapper; nullsLast handles the null cases itself and only delegates when both arguments are non-null, so the length extractor is never applied to a null and no NPE occurs.

    2. B. [null, null, bob, amy, cara]

      That is nullsFirst output; it encodes the belief that the wrapper name describes where the nulls come from rather than where they end up — nullsLast sinks them to the end.

    3. C. [amy, bob, cara, null, null]

      Assumes a comparator on length breaks ties alphabetically; a comparator that returns 0 leaves the order to the sort, and List.sort is stable, so bob (input first) stays ahead of amy.

    4. D. [bob, amy, cara, null, null]Correct answer

      Correct: nullsLast sends the nulls to the end, and among the non-nulls comparing by length orders bob(3), amy(3), cara(4) with the stable sort keeping bob before amy on the length-3 tie.

    Explanation

    Trace: nullsLast(cmp) returns a comparator that handles the null cases itself — null is greater than any non-null, two nulls are equal — and only delegates to the wrapped comparator when *both* arguments are non-null. So String::length is never applied to a null. Among the non-nulls, comparing(String::length) gives bob=3, amy=3, cara=4, and List.sort is a stable sort: bob and amy tie on length 3 and therefore keep their input order (bob came first). The nulls sink to the end, and the run prints `[bob, amy, cara, null, null]`. Why the others are wrong: `[null, null, bob, amy, cara]` is what nullsFirst produces — the same list sorted with Comparator.nullsFirst(...) really does print that. It encodes the belief that the wrapper name describes where the nulls *come from* rather than where they end up. `[amy, bob, cara, null, null]` assumes that a comparator on length breaks its ties alphabetically. It does not: a comparator that returns 0 leaves the order to the sort, and a stable sort preserves the encounter order, so bob stays ahead of amy. `Throws NullPointerException` is what you get if you drop the nullsLast wrapper — `names.sort(Comparator.comparing(String::length))` on this list really does throw NPE when the key extractor is handed a null. That is exactly the failure nullsLast exists to prevent. Exam tip: Comparator.nullsFirst/nullsLast is the only sanctioned way to sort a collection containing nulls; the key-extractor comparators (comparing, comparingInt) call the extractor on every element and will NPE. And remember that List.sort/Collections.sort are contractually *stable*, so tied elements never get shuffled — which is why the tie-break here is "input order", not "alphabetical".

  8. Question 8

    What is the result? ```java import java.util.*; public class Main { public static void main(String[] args) { Map<String,Integer> m = Map.of("a", 1, "b", 2); m.put("c", 3); System.out.println(m.size()); } } ```

    1. A. 2

      This treats put as a silent no-op that leaves the map unchanged. The immutable implementation does not quietly ignore the mutator; it throws instead of returning.

    2. B. 3

      This assumes the new entry was actually added. Growing the map that way requires a mutable map such as new HashMap<>(Map.of(...)), not the unmodifiable map returned by Map.of.

    3. C. Compilation fails: put is not defined on the returned map

      put is declared on the Map interface, so the call is fully visible and type-correct. Immutability is a runtime property of the implementation, never a compile-time restriction.

    4. D. Throws UnsupportedOperationExceptionCorrect answer

      Map.of returns an unmodifiable map. The put call compiles because put is declared on Map, but the immutable implementation rejects every mutator at runtime, throwing UnsupportedOperationException before size() is ever reached.

    Explanation

    The `List.of` / `Set.of` / `Map.of` family returns unmodifiable, null-rejecting collections. Any mutator such as put, add, remove, or clear compiles, because it is declared on the collection interface, but the immutable implementation throws UnsupportedOperationException at runtime rather than at compile time. Here the exception is raised on the put call, before `size()` can run. Wrapping the result in a mutable copy such as `new HashMap<>(...)` is the standard escape hatch.

Practise all 20 Generics and Collections questions

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

Open OCP Java SE 25

Other topics in this pack