Generics and Collections practice questions

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

Generics and Collections practice questions from OCP Java SE 17 (1Z0-829). This pack has 22 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

    The list below is sorted with a Comparator constant declared in java.lang.String. What is printed? ```java import java.util.*; public class Main { public static void main(String[] args) { List<String> names = new ArrayList<>(List.of("Delta", "echo", "Alpha", "bravo")); names.sort(String.CASE_INSENSITIVE_ORDER); System.out.println(names); } } ```

    1. A. [Alpha, Delta, bravo, echo]

      This is the natural (compareTo) ordering, where all upper-case letters sort before all lower-case ones because of their UTF-16 code points — exactly the pitfall the case-insensitive comparator avoids.

    2. B. [Delta, echo, Alpha, bravo]

      This is the unsorted input order; the list is actually reordered by the comparator.

    3. C. [Alpha, bravo, Delta, echo]Correct answer

      Correct — the case-insensitive comparator folds case before comparing, giving alpha < bravo < delta < echo while leaving each element's original capitalisation intact.

    4. D. [alpha, bravo, delta, echo]

      Assumes sorting lower-cases the elements; a comparator only decides order and never mutates the strings, so their original capitalisation is preserved.

    Explanation

    The case-insensitive comparator compares characters after folding their case, so ordering follows the alphabet regardless of capitalisation, unlike natural String ordering where every upper-case letter precedes every lower-case one. Crucially, a comparator only determines order and never alters the elements, so each string keeps the capitalisation it started with.

  2. Question 2

    What does this print? ```java import java.util.*; public class Main { public static void main(String[] args) { Map<String,Integer> m = new HashMap<>(); System.out.println(m.getOrDefault("x", 0) + " " + m.computeIfAbsent("x", k -> 7) + " " + m.get("x")); } } ```

    1. A. 7 7 7

      This assumes getOrDefault runs after the insertion. Operands are evaluated left to right, so getOrDefault executes while the map is still empty and returns the default 0, not 7.

    2. B. 0 7 7Correct answer

      getOrDefault returns the default 0 without inserting anything, computeIfAbsent then computes 7, stores it, and returns 7, and the final get sees the inserted 7 (Javadoc, Map.getOrDefault / computeIfAbsent).

    3. C. 0 7 null

      This assumes computeIfAbsent returns a value without storing it. It actually inserts the computed value under the key, so the final get returns 7 rather than null.

    4. D. 0 0 0

      This pretends computeIfAbsent neither computes nor stores anything. When the key is absent it does both, returning and inserting 7.

    Explanation

    The two lookups differ in whether they mutate the map. getOrDefault reads without ever inserting, so on an empty map it returns the supplied default 0 and leaves the map empty. computeIfAbsent, finding no mapping, runs the function, stores the computed value under the key, and returns it, so a subsequent get retrieves that stored value. Because the three operands are evaluated left to right, the default read happens before the insertion.

  3. Question 3

    Two calls to remove() are made on the same List<Integer>. What is the final content of the list? ```java import java.util.*; public class Main { public static void main(String[] args) { List<Integer> l = new ArrayList<>(List.of(10, 20, 30, 40)); l.remove(1); l.remove(Integer.valueOf(30)); System.out.println(l); } } ```

    1. A. [10, 20, 40]

      Assumes the int argument searches for the value 1; it selects the index-based overload, which deletes the element at index 1 (the value 20).

    2. B. [10, 30, 40]

      Shows only the first removal and assumes the second fails; object-based removal uses equals(), which matches 30 regardless of the Integer cache, so it is removed too.

    3. C. Throws IndexOutOfBoundsException

      Assumes both calls are index-based; the second argument is an Integer reference, which binds to the object-based overload and searches by value, not position.

    4. D. [10, 40]Correct answer

      Correct — the int argument selects the index-based overload and deletes index 1 (value 20), then the boxed Integer selects the object-based overload and deletes the element equal to 30, leaving [10, 40] (JLS 17 §15.12.2).

    Explanation

    The list has two remove overloads — one taking an index, one taking an object — and overload resolution's first phase performs no boxing, so a bare int literal binds to the index-based overload and deletes by position, while a boxed Integer argument binds to the object-based overload and deletes the first element equal to it. Object-based removal uses equals(), so it matches by value irrespective of any wrapper caching.

  4. Question 4

    Which is true of a method declared <T extends Comparable<T>> T max(List<T> list)?

    1. A. It can be called only with List<Object>

      List<Object> does not satisfy the bound because Object is not Comparable; the method accepts self-comparable element types such as List<Integer> or List<String>.

    2. B. T is a type parameter bounded so its elements can be compared with one another via compareToCorrect answer

      The bound <T extends Comparable<T>> restricts T to types comparable with their own kind, so inside the method compareTo may be called between elements; any self-comparable type such as Integer, String, or LocalDate qualifies (JLS 4.4, bounded type parameters).

    3. C. It cannot return a value of type T

      T is the declared return type, so the method can and does return a value of type T; returning the maximal element is the whole point of the signature.

    4. D. The bound means T must be exactly Comparable

      In a type-parameter bound, extends means 'is a subtype of' (including implementing an interface), not 'is exactly', so T need not be Comparable itself, only implement it.

    Explanation

    A bounded type parameter <T extends Comparable<T>> constrains the argument type to ones that can compare against their own kind, which is exactly what lets the method invoke compareTo between elements and return the largest as type T. The bound admits any self-comparable type, not only one specific class, and it is written with extends even though Comparable is an interface. A raw Object element type does not qualify because it is not Comparable.

  5. Question 5

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

    1. A. Throws UnsupportedOperationExceptionCorrect answer

      List.of returns an unmodifiable list. The call compiles because add is declared on the List interface, but the returned implementation rejects every mutator, so add throws UnsupportedOperationException at runtime before the println runs (Javadoc 17, List.of immutable lists).

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

      This assumes a mutable list. Producing [1, 2, 3, 4] would require a modifiable list such as new ArrayList<>(List.of(1, 2, 3)); the unmodifiable list never accepts the added element.

    3. C. [1, 2, 3]

      This assumes the add silently no-ops and leaves the list unchanged. Unmodifiable collections fail fast with an exception rather than ignoring a mutator call.

    4. D. Compilation fails: add not visible

      This mistakes a runtime failure for a visibility error. add is part of the List interface, so the code compiles cleanly; the failure happens only when it executes.

    Explanation

    List.of returns an unmodifiable list. The add call compiles because add is declared on the List interface, but the implementation returned by List.of rejects every mutator, so the add throws UnsupportedOperationException at runtime and the println is never reached. List.of/Set.of/Map.of, List.copyOf, and Collectors.toUnmodifiableList all return unmodifiable collections whose add/remove/set throw the same exception; List.of additionally rejects null elements with NullPointerException.

  6. Question 6

    A method is declared void copy(List<? super Number> dst, List<? extends Number> src). Which two statements are correct? (Choose two.)

    1. A. Inside copy, dst.add(1) compilesCorrect answer

      dst is declared List<? super Number>, so its element type is Number or a supertype; any Number, including the autoboxed Integer 1, can safely be added, so dst.add(1) compiles (JLS 17 4.5.1, PECS).

    2. B. Inside copy, src.add(1) compiles

      src is a producer declared (? extends Number) with an unknown exact element type (it could be a List<Double>), so the compiler rejects adding anything but the null literal.

    3. C. A List<Integer> is a valid argument for srcCorrect answer

      src is (? extends Number), which accepts any list whose element type is a subtype of Number, so a List<Integer> is a valid argument (JLS 17 4.5.1).

    4. D. A List<Integer> is a valid argument for dst

      dst requires a supertype of Number, and Integer is a subtype, so a List<Integer> is rejected; otherwise copy could store a Double into a list of Integers.

    Explanation

    Under PECS, a lower-bounded consumer (? super Number) can have any Number and its subtypes added to it but is read back only as Object, while an upper-bounded producer (? extends Number) can supply elements read as Number but forbids non-null additions. So the destination safely accepts an added Number, and the source safely accepts any Number-subtype list as its argument. The mirror-image operations, adding to the producer or passing a subtype list where a supertype is required, are the ones that fail to compile.

  7. Question 7

    Two overloads of describe() differ only in the type argument of their List parameter. What is the result of compiling and running this program? ```java import java.util.*; public class Main { static String describe(List<String> list) { return "strings:" + list.size(); } static String describe(List<Integer> list) { return "ints:" + list.size(); } public static void main(String[] args) { System.out.println(describe(new ArrayList<String>())); } } ```

    1. A. strings:0

      This is what a successful call to the String-list overload would print, but the two overloads cannot coexist, so the class never compiles.

    2. B. ints:0

      Assumes the Integer-list overload is chosen and the program runs; the duplicate-erasure declarations fail to compile before any call is resolved.

    3. C. Compilation failsCorrect answer

      Correct — both overloads erase to the same raw signature, which is a name clash the compiler rejects; the error is in the declarations themselves, independent of any call (JLS 17 §8.4.2, §4.6).

    4. D. Throws ClassCastException

      Assumes a runtime type mismatch; there is no runtime, because the overloaded declarations do not compile.

    Explanation

    Generic type arguments are erased, so two methods that differ only in the type argument of the same parameter type reduce to identical raw signatures — an override-equivalent clash the compiler forbids. The error lives in the declarations, so it occurs whether or not either method is ever called, and differing return types would not resolve it; only genuinely different erasures or different names would.

  8. Question 8

    After compilation, what does type erasure do to the generic method <T> T first(List<T> list)?

    1. A. The compiler generates a separate compiled method for each type argument used by callers

      Java does not instantiate generics per type argument the way C++ templates do; one erased method serves every caller.

    2. B. T is replaced by its bound (Object when unbounded) and the compiler inserts casts at call sitesCorrect answer

      Erasure replaces each type variable with its leftmost bound (Object for an unbounded T), so the bytecode signature is effectively Object first(List list), and the compiler inserts synthetic checked casts at call sites (JLS 17 4.6, Type Erasure).

    3. C. The type argument is stored in each List object and checked by the JVM at runtime

      Type arguments are erased and not stored in the collection object, and the JVM performs no generic type checks at runtime.

    4. D. List<String> and List<Integer> arguments have different runtime classes

      A List<String> and a List<Integer> share the same runtime class (for example java.util.ArrayList); getClass() carries no type-argument information.

    Explanation

    Generics are a compile-time-only feature: type erasure rewrites each type variable to its leftmost bound (Object when unbounded), so the compiled method carries no type argument and the compiler adds synthetic casts where callers use its result. Nothing about the type argument survives into the bytecode or the runtime object, so there is one shared method and one runtime class per raw type. This is why constructs like new T(), T.class, and instanceof List<String> are illegal, and why two overloads differing only in type arguments clash.

Practise all 22 Generics and Collections questions

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

Open OCP Java SE 17

Other topics in this pack