What is the difference between List wildcard and List Object
The distinction between List> (unbounded wildcard) and List
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
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 ofList<Object>. Passing aList<String>to a parameter of typeList<Object>triggers a compilation error.
- Explicitly parameterized to hold instances of
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 wildcardList<?>. 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 acceptList<String>.List<?>is the universal supertype of all lists. Accepts anyList<T>; forbids adding objects (exceptnull); reads returnObject.
- Reason for Invariance: Prevents heap pollution and runtime
ClassCastException. List<?>is NOT Immutable: It permits structural mutations:remove(index),clear(), andadd(null).- Capture Helper Pattern: When mutating or swapping elements within a
List<?>, delegate to aprivate static <T> void helper(List<T> list)method. - Instantiation Rule:
new ArrayList<?>is a syntax error. Usenew ArrayList<Object>()or diamond syntaxnew ArrayList<>(). - Legal Arrays:
new List<?>[10]is permitted becauseList<?>is a reifiable type.