📋 Section 20 · Question #21

What is the difference between List wildcard and List Object

The distinction between List (unbounded wildcard) and List represents one of the most fundamental concepts in Java's generic type system, governing type safety, invar...

The distinction between List<?> (unbounded wildcard) and List<Object> represents one of the most fundamental concepts in Java’s generic type system, governing type safety, invariance, and mutation permissions.


🟢 Junior Level

Core Differences

  1. List<Object> — A Concrete Invariant List of Objects:
    • Explicitly parameterized to hold instances of java.lang.Object.
    • Allows inserting any reference type (String, Integer, custom domain models) via .add().
    • Strictly Invariant: List<String> is not a subtype of List<Object>. Passing a List<String> to a parameter of type List<Object> triggers a compilation error.
  2. List<?> (Unbounded Wildcard) — A List of an Unknown Type:
    • Signifies a list holding elements of some specific, concrete type, but the compiler does not know what that type is.
    • Serves as the universal supertype for all parameterized lists (List<String>, List<Integer>, List<Object>).
    • Prohibits Inserting Elements: The compiler forbids adding any object (except the literal null) because it cannot guarantee type compatibility.
    • Reading elements safely yields java.lang.Object.
List<String> names = new ArrayList<>(List.of("Alice", "Bob"));

// ❌ Compilation Error: List<String> cannot be assigned to List<Object>
List<Object> objList = names; 

// ✅ Valid: List<?> is the supertype of all List<T>
List<?> wildcardList = names; 

// ❌ Compilation Error: Cannot add elements to List<?>
wildcardList.add("Charlie"); 

// ✅ Allowed: null is compatible with any reference type
wildcardList.add(null); 

// ✅ Reading is safe; returns Object
Object first = wildcardList.get(0);

🟡 Middle Level

Why List<String> is Not a Subtype of List<Object> (Invariance)

If the Java compiler allowed List<Object> list = new ArrayList<String>(), type safety would collapse at runtime:

// Hypothetical scenario IF generics were covariant:
List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; // If this were allowed...

// Adding an Integer into a list of objects seems valid:
objectList.add(Integer.valueOf(42)); 

// Catastrophe: The underlying list in heap memory is polluted!
String str = stringList.get(0); // 💥 ClassCastException: Integer cannot be cast to String!

To eliminate this vulnerability, Java generics enforce invariance: List<Sub> is never assignable to List<Super>. In contrast, legacy Java arrays are covariant (Object[] arr = new String[5];), deferring type mismatch checks to runtime ArrayStoreException errors.

Wildcard Capture and the Mutation Barrier

The syntax List<?> is equivalent to List<? extends Object>. When the compiler encounters an invocation of list.add(E element) on a List<?>, the type $E$ is captured as an internal, anonymous type variable: capture of ? (or CAP#1 extends Object).

void insert(List<?> list) {
    // Compilation Error: 
    // incompatible types: String cannot be converted to capture#1 of ?
    list.add("test"); 
}

The compiler reasons that list could be an instance of ArrayList<Integer>. Because it cannot prove that "test" is compatible with the unknown captured type CAP#1, it rejects the operation. Only null is accepted because null can represent any reference type.

Comparison Table

Feature List (Raw Type) List<Object> List<?> (Wildcard)
Compiler Type Safety 🔴 Disabled (unchecked warnings) 🟢 100% Type-safe 🟢 100% Type-safe
Adding Elements Accepts any Object Accepts any Object Prohibited (only null allowed)
Reading Elements Returns bare Object (requires manual cast) Returns Object Returns Object
Assignment Compatibility Accepts any List (unchecked) Accepts only List<Object> Accepts any List<T>
Intended Use Case Legacy pre-Java 5 interoperability only Heterogeneous collections storing diverse types Read-only structural utilities (size(), print())

🔴 Senior Level

The Wildcard Capture Helper Pattern

Because List<?> blocks element mutations, developers often get stuck when implementing operations like reversing or swapping elements in a wildcard collection:

// Compilation Error: capture of ?
public static void swap(List<?> list, int i, int j) {
    Object temp = list.get(i);
    // list.set(i, list.get(j)); // ❌ set(int, capture#1 of ?) cannot accept capture#2 of ?
}

To solve this cleanly while preserving external API simplicity, the standard library uses a Capture Helper:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j); // Wildcard is captured into type parameter T
}

// Private generic helper binds the wildcard to named type parameter T:
private static <T> void swapHelper(List<T> list, int i, int j) {
    list.set(i, list.set(j, list.get(i))); // Fully type-safe!
}

Instantiation Restrictions: new ArrayList<?> vs. new ArrayList<Object>

Under JLS §15.9, wildcards cannot be used in constructor instance creation expressions:

// ❌ Compilation Error: Wildcard cannot be instantiated directly
List<?> list1 = new ArrayList<?>(); 

// ✅ Valid: Concrete type argument required for instantiation
List<?> list2 = new ArrayList<Object>();
List<?> list3 = new ArrayList<String>();
List<?> list4 = new ArrayList<>(); // Diamond operator infers Object

The new operator requires a concrete, reifiable or clearly bounded type to allocate memory structures on the JVM heap. Wildcards are strictly restricted to declaration sites and method parameter boundaries (Use-Site Variance).


4 Tricky Questions

1. Is List<?> truly an immutable or read-only collection?

Answer: No, List<?> is neither immutable nor completely read-only. The wildcard constraint only restricts methods that take a generic type parameter as an argument (such as add(E), addAll(Collection<? extends E>), and set(int, E)).

However, methods that do not depend on the generic parameter work normally and mutate the underlying list:

List<?> list = new ArrayList<>(List.of(1, 2, 3));
list.remove(0); // Mutates the list by index!
list.clear();   // Empties the list!
list.add(null); // Inserts null!

To achieve true immutability, wrap the collection using Collections.unmodifiableList(list) or List.copyOf(list).

2. What is the difference between void process(List<?> list) and <T> void process(List<T> list), and when should you choose one over the other?

Answer:

  • <T> void process(List<T> list) declares a named type parameter $T$. This is necessary if the method establishes a dependency between multiple parameters (e.g., void copy(List<T> src, List<T> dest)), uses $T$ in its return type, or instantiates local variables of type $T$.
  • void process(List<?> list) does not declare a type parameter. According to Effective Java (Item 31), if a type parameter appears only once in a method declaration and has no relationship to other arguments or the return value, prefer the wildcard List<?>. It simplifies the API and makes the signature cleaner for callers.

3. Can you pass a List<String> to a method expecting List<Object> using explicit casting? What happens?

Answer: Direct casting (List<Object>) stringList triggers a compilation error: incompatible types: List<String> cannot be converted to List<Object>.

A developer can force the cast via a raw type or an intermediate wildcard:

List<Object> dangerous = (List<Object>) (List<?>) stringList; // Unchecked cast warning
dangerous.add(Integer.valueOf(100)); // Inserts Integer into List<String>

While the write succeeds, heap pollution is created. The moment the original owner of List<String> retrieves that element (String s = stringList.get(0);), the JVM’s compiler-inserted checkcast throws an immediate ClassCastException.

4. Why does the compiler allow List<?>[] arrayOfLists = new ArrayList<?>[10], but forbids new ArrayList<String>[10]?

Answer: Generic array creation is forbidden for non-reifiable types because array covariance combined with type erasure allows heap pollution that bypasses ArrayStoreException.

List<String> is non-reifiable because its type argument is erased to raw List at runtime. In contrast, List<?> is an unbounded wildcard type, which is a reifiable type in the Java type system (its runtime representation is identical to its compile-time representation). Because callers can never insert elements into List<?> (except null), array covariance cannot corrupt the underlying elements. Therefore, new ArrayList<?>[10] is safe and legally permitted.


🎯 Interview Cheat Sheet

  • Core Difference:
    • List<Object> is an invariant list of objects. Allows adding any object; does not accept List<String>.
    • List<?> is the universal supertype of all lists. Accepts any List<T>; forbids adding objects (except null); reads return Object.
  • Reason for Invariance: Prevents heap pollution and runtime ClassCastException.
  • List<?> is NOT Immutable: It permits structural mutations: remove(index), clear(), and add(null).
  • Capture Helper Pattern: When mutating or swapping elements within a List<?>, delegate to a private static <T> void helper(List<T> list) method.
  • Instantiation Rule: new ArrayList<?> is a syntax error. Use new ArrayList<Object>() or diamond syntax new ArrayList<>().
  • Legal Arrays: new List<?>[10] is permitted because List<?> is a reifiable type.

☕ Java Interview Questions and Answers · Open Source Knowledge Portal

Built from the GitHub repository. Fully synced across English, Ukrainian, and Russian.