Java Wildcards (?)
Java Wildcards (?)
The wildcard ? in Java generics represents an unknown type. Where a type parameter <T> names the unknown so it can be referred to multiple times, the wildcard ? says "some type, but I neither know nor need to name it." List<?> means "a list of some type of element — I do not know what type, and I am not going to commit to one." This might sound less useful than naming the type, but it solves a specific problem that named type parameters cannot: expressing that a method should accept a collection of any element type when the method does not need to put anything back in or create elements of that type.
What Is a Wildcard?
The wildcard ? is a type argument — it appears where a type argument goes, inside angle brackets — but unlike a named type parameter T, it cannot be referenced by name anywhere else in the same declaration. It has three forms:
UNBOUNDED WILDCARD:
List<?> "a list of some unknown type"
Collection<?> "a collection of some unknown type"
Class<?> "a Class object for some unknown type"
Can read from it: every element comes out as Object
Cannot add to it: the compiler rejects any add() call
(except null, which is always safe)
UPPER-BOUNDED WILDCARD:
List<? extends Number> "a list of some type that is Number or a subtype"
(covered in the Upper-Bounded Wildcards article)
LOWER-BOUNDED WILDCARD:
List<? super Integer> "a list of some type that is Integer or a supertype"
(covered in the Lower-Bounded Wildcards article)
THIS ARTICLE FOCUSES ON THE UNBOUNDED WILDCARD: List<?>
Basic Overview - Four Things to Understand About the Unbounded Wildcard
1. WHY ? EXISTS: THE INVARIANCE PROBLEM
Fresher view : you might expect that because Integer extends Number,
a List<Integer> should be usable where a List<Number>
is expected. It is NOT. Generic types are invariant -
List<Integer> and List<Number> are unrelated types.
List<?> solves this: it says "a list of ANY type",
and a List<Integer>, List<Number>, or List<String>
can all be passed where List<?> is expected.
Deeper view : the invariance rule exists because allowing
List<Integer> where List<Number> is expected would
let you add a Double (a Number) to what is actually
a List<Integer>, corrupting it silently. List<?> is
safe because it disables BOTH reading with a type
AND writing - elements come out as Object, and
nothing (except null) can go in.
2. WHAT YOU CAN DO WITH List<?>
Fresher view : you can iterate over it, call size(), call isEmpty(),
call contains(), pass it to methods that accept
List<?>, and read every element as Object.
You CANNOT call add(anything except null) on it.
Deeper view : the restriction on add() is not a runtime limitation -
it is a compile-time check. The compiler refuses
add(item) on List<?> because it cannot verify that
item is the correct type for the unknown element type.
Even if you know the list contains Strings, you cannot
add a String through a List<?> reference - the type
information has been deliberately erased from the
reference's perspective.
3. List<?> vs List<Object>
Fresher view : they look similar but behave very differently.
List<Object> means the list holds Object values -
you can add anything to it (everything is an Object).
List<?> means the list holds SOME unknown type -
you cannot add anything (you do not know what type
is accepted).
Deeper view : List<Object> accepts ONLY List<Object> - not
List<String>, not List<Integer>. This is generic
invariance at work. List<?> accepts List<String>,
List<Integer>, List<Object>, and any other
parameterized List. This is why a utility method
that only reads a list should declare List<?>,
not List<Object> - the former accepts any list,
the latter accepts almost none.
4. ? vs T - WHEN TO USE WHICH
Fresher view : use T when you need to REFER to the type again
in the same declaration - in the return type,
in another parameter, or inside the body.
Use ? when you do not need to name the type at
all - the method only reads elements as Object,
or only checks size/emptiness.
Deeper view : ? is syntactic sugar for a bounded or unbounded
type variable that has been "thrown away" -
you cannot say "give me back a T" because you
never captured T. If you need to do anything
with the element beyond treating it as Object,
you need a named type parameter instead.
The Invariance Problem That Wildcards Solve
Generic types in Java are invariant: List<String> and List<Integer> are completely unrelated types, even though String and Integer both extend Object. This surprises many developers who expect the relationship between element types to carry into the parameterized type.
// File: InvarianceProblemDemo.java
import java.util.ArrayList;
import java.util.List;
public class InvarianceProblemDemo {
// A method that prints every element.
// Instinct says: "accept List<Object> - everything IS an Object"
static void printAllBroken(List<Object> items) {
for (Object item : items) {
System.out.println(" " + item);
}
}
// The wildcard fix: List<?> accepts a list of ANY element type
static void printAll(List<?> items) {
for (Object item : items) { // element type is unknown - comes out as Object
System.out.println(" " + item);
}
}
public static void main(String[] args) {
List<String> productNames = List.of("Wireless Mouse", "Laptop Stand", "USB Hub");
List<Integer> stockCounts = List.of(120, 45, 200);
List<Double> prices = List.of(799.0, 1299.0, 499.0);
System.out.println("=== printAll(List<?>) - accepts any List ===");
printAll(productNames); // List<String> passed as List<?> - works
System.out.println(" ---");
printAll(stockCounts); // List<Integer> passed as List<?> - works
System.out.println(" ---");
printAll(prices); // List<Double> passed as List<?> - works
System.out.println();
System.out.println("=== The List<Object> approach fails at compile time ===");
// printAllBroken(productNames); // COMPILE ERROR:
// "incompatible types: List<String> cannot be converted to List<Object>"
// List<String> is NOT a List<Object> even though String IS an Object
// Generic types are INVARIANT - the subtype relationship on elements
// does NOT transfer to the parameterized type
// Only an actual List<Object> works with List<Object> parameter
List<Object> mixed = new ArrayList<>();
mixed.add("A String");
mixed.add(42);
printAllBroken(mixed); // compiles - this really is List<Object>
System.out.println();
System.out.println("=== What you CAN do with List<?> ===");
List<String> sample = new ArrayList<>(List.of("Swiggy", "Zomato", "Blinkit"));
List<?> wildcard = sample; // assigning List<String> to List<?> - legal
System.out.println("size() : " + wildcard.size());
System.out.println("isEmpty() : " + wildcard.isEmpty());
System.out.println("contains : " + wildcard.contains("Swiggy"));
Object first = wildcard.get(0); // returns Object - no type known
System.out.println("get(0) : " + first);
// wildcard.add("New Item"); // COMPILE ERROR
// "no suitable method found for add(String)"
// The compiler cannot verify "New Item" is the right type
wildcard.add(null); // null is the one exception - always safe
System.out.println("add(null) : succeeded (null is always safe)");
}
}Output:
=== printAll(List<?>) - accepts any List ===
Wireless Mouse
Laptop Stand
USB Hub
---
120
45
200
---
799.0
1299.0
499.0
=== The List<Object> approach fails at compile time ===
A String
42
=== What you CAN do with List<?> ===
size() : 3
isEmpty() : false
contains : true
get(0) : Swiggy
add(null) : succeeded (null is always safe)
Common Uses of the Unbounded Wildcard
Utility Methods That Only Read
Any method that only needs to read elements from a collection — printing, counting, searching, logging — should accept List<?> (or Collection<?>) rather than List<Object>. The wildcard makes the method genuinely generic: it works with any parameterized collection, not just List<Object>.
// File: ReadOnlyWildcardDemo.java
import java.util.*;
public class ReadOnlyWildcardDemo {
// Counts how many elements satisfy a condition.
// Collection<?> accepts Collection<String>, Collection<Integer>, etc.
static int countNonNull(Collection<?> items) {
int count = 0;
for (Object item : items) {
if (item != null) count++;
}
return count;
}
// Returns a formatted summary line for any list.
static String summarize(List<?> items) {
if (items.isEmpty()) return "Empty list";
Object first = items.get(0);
Object last = items.get(items.size() - 1);
return "List[size=" + items.size()
+ ", first=" + first
+ ", last=" + last + "]";
}
// Checks if two lists have the same size.
// Both can be lists of completely different element types.
static boolean sameSizeAs(List<?> first, List<?> second) {
return first.size() == second.size();
}
// Prints a map's contents where keys and values can be any types.
static void printMap(Map<?, ?> map) {
map.forEach((key, value) ->
System.out.println(" " + key + " -> " + value));
}
public static void main(String[] args) {
List<String> cities = List.of("Mumbai", "Delhi", "Bengaluru");
List<Integer> codes = List.of(400001, 110001, 560001);
List<Double> rates = List.of(7.5, 8.1, null, 6.9);
System.out.println("=== countNonNull ===");
System.out.println("cities non-null: " + countNonNull(cities));
System.out.println("rates non-null : " + countNonNull(rates));
System.out.println();
System.out.println("=== summarize ===");
System.out.println(summarize(cities));
System.out.println(summarize(codes));
System.out.println(summarize(List.of()));
System.out.println();
System.out.println("=== sameSizeAs ===");
System.out.println("cities and codes same size? " + sameSizeAs(cities, codes));
System.out.println("cities and rates same size? " + sameSizeAs(cities, rates));
System.out.println();
System.out.println("=== printMap with Map<String, Integer> ===");
Map<String, Integer> stockMap = new LinkedHashMap<>();
stockMap.put("Wireless Mouse", 120);
stockMap.put("Laptop Stand", 45);
stockMap.put("USB Hub", 200);
printMap(stockMap);
System.out.println("=== printMap with Map<Integer, String> ===");
Map<Integer, String> idToCity = Map.of(1, "Mumbai", 2, "Pune", 3, "Nashik");
printMap(idToCity);
}
}Output:
=== countNonNull ===
cities non-null: 3
rates non-null : 3
=== summarize ===
List[size=3, first=Mumbai, last=Bengaluru]
List[size=3, first=400001, last=560001]
Empty list
=== sameSizeAs ===
cities and codes same size? true
cities and rates same size? false
=== printMap with Map<String, Integer> ===
Wireless Mouse -> 120
Laptop Stand -> 45
USB Hub -> 200
=== printMap with Map<Integer, String> ===
1 -> Mumbai
2 -> Pune
3 -> Nashik
Class<?> - The Most Common Non-List Wildcard
Outside collections, Class<?> is the wildcard you encounter most often. Reflection methods like getDeclaredMethods(), forName(), and annotation reading return or accept Class<?> because the actual class type is unknown at compile time.
// File: ClassWildcardDemo.java
import java.lang.reflect.Method;
public class ClassWildcardDemo {
static class InventoryService {
public void updateStock(String productId, int quantity) {}
public int getStock(String productId) { return 0; }
private void reconcileInternal() {}
}
// Accepts Class<?> - works for any class, not just a specific one.
// Used in frameworks and DI containers that inspect arbitrary classes.
static void describeClass(Class<?> clazz) {
System.out.println("Class : " + clazz.getSimpleName());
System.out.println("Package : " + clazz.getPackageName());
System.out.println("Interface?: " + clazz.isInterface());
Method[] methods = clazz.getDeclaredMethods();
System.out.println("Methods : " + methods.length);
for (Method method : methods) {
System.out.println(" -> " + method.getName()
+ " (" + method.getParameterCount() + " params)");
}
}
// Creates an instance of any no-arg class using reflection.
// The return type Object is all that can be offered - the actual
// type is unknown, which is exactly why Class<?> is used.
static Object createInstance(Class<?> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
public static void main(String[] args) throws Exception {
System.out.println("=== describeClass with InventoryService ===");
describeClass(InventoryService.class);
System.out.println();
System.out.println("=== describeClass with String (from the JDK) ===");
// Class<?> - works for any class, whether ours or JDK's
Class<?> stringClass = String.class;
System.out.println("Class : " + stringClass.getSimpleName());
System.out.println("Is final : " + java.lang.reflect.Modifier.isFinal(
stringClass.getModifiers()));
System.out.println();
System.out.println("=== createInstance ===");
Object instance = createInstance(StringBuilder.class);
System.out.println("Created : " + instance.getClass().getSimpleName());
System.out.println("Instance : " + instance);
}
}Output:
=== describeClass with InventoryService ===
Class : InventoryService
Package :
Interface?: false
Methods : 3
-> updateStock (2 params)
-> getStock (1 params)
-> reconcileInternal (0 params)
=== describeClass with String (from the JDK) ===
Class : String
Is final : true
=== createInstance ===
Created : StringBuilder
Instance :
When to Choose ? Over a Named Type Parameter
The decision between ? and <T> is about whether the type needs to be named and reused. If the method only reads elements (treating each as Object) and never references the type anywhere else, ? is the cleaner choice. The moment the type needs to appear in more than one place in the same declaration, a named parameter becomes necessary.
USE ? WHEN:
The type appears in exactly ONE position and is never referenced again:
static void print(List<?> items) { ... }
// ? appears once - perfect use case
The method only checks structural properties (size, isEmpty):
static boolean hasThreePlus(Collection<?> c) { return c.size() >= 3; }
The method receives a Class<?> for reflection work:
static void inspect(Class<?> clazz) { ... }
A field stores a generic container of unknown type:
private List<?> snapshot; // the type was lost at some API boundary
USE <T> WHEN:
The type must appear in TWO or more positions - as a parameter AND
as the return type, or in two parameters that must match:
static <T> T getFirst(List<T> items) // T appears twice
static <T> List<T> copy(List<T> source) // T appears twice
static <T> void swap(T[] arr, int i, int j) // T appears twice
static <T> boolean contains(List<T> list, T target) // T appears twice
The body needs to call T-specific methods or create T instances:
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b; // compareTo needs <T extends Comparable<T>>
}
Wildcard Capture - How the Compiler Works With ?
When you pass a List<?> to a helper method that uses a named type parameter, the compiler performs wildcard capture — it treats the ? as a specific (but unknown) type for the duration of that call. This allows a private helper method to work with the unknown type in a way the public method with ? cannot.
// File: WildcardCaptureDemo.java
import java.util.List;
public class WildcardCaptureDemo {
// PUBLIC API: accepts any List<?> - the unknown type is captured
// and passed to the helper where it can be named
public static void reverse(List<?> list) {
reverseHelper(list); // compiler captures ? as a specific type T
}
// PRIVATE HELPER: the captured type is given a name <T>
// This is the only way to write algorithms that need to name the type
// of elements in a wildcard list
private static <T> void reverseHelper(List<T> list) {
int left = 0;
int right = list.size() - 1;
while (left < right) {
T temp = list.get(left);
list.set(left, list.get(right));
list.set(right, temp);
left++;
right--;
}
}
// Another example: swap two elements in a wildcard list
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
public static void main(String[] args) {
java.util.List<String> cities = new java.util.ArrayList<>(
java.util.List.of("Mumbai", "Pune", "Nashik", "Aurangabad"));
System.out.println("Before reverse: " + cities);
reverse(cities); // List<String> passed as List<?> - capture happens
System.out.println("After reverse : " + cities);
System.out.println();
java.util.List<Integer> amounts = new java.util.ArrayList<>(
java.util.List.of(500, 1200, 800, 3000));
System.out.println("Before swap: " + amounts);
swap(amounts, 0, 3); // List<Integer> passed as List<?> - capture happens
System.out.println("After swap : " + amounts);
}
}Output:
Before reverse: [Mumbai, Pune, Nashik, Aurangabad]
After reverse : [Aurangabad, Nashik, Pune, Mumbai]
Before swap: [500, 1200, 800, 3000]
After swap : [3000, 1200, 800, 500]
Wildcard capture is what lets algorithms that need a named type variable work behind a ?-based public API. reverse(List<?> list) is the clean public signature — callers can pass any list. reverseHelper(List<T> list) is where the actual mutation happens using the named type. The compiler connects the two by treating the ? captured from the first as the T in the second.
Basic Example
A single printAll method that accepts a list of any element type, using the unbounded wildcard List<?>.
import java.util.List;
public class Main {
static void printAll(List<?> items) {
for (Object item : items) {
System.out.println(item);
}
}
public static void main(String[] args) {
List<String> cities = List.of("Mumbai", "Delhi");
List<Integer> codes = List.of(400001, 110001);
printAll(cities);
printAll(codes);
}
}Output
Mumbai Delhi 400001 110001
How it works:
- ›
List<?>means "a list of some unknown type," soprintAllacceptsList<String>,List<Integer>, or any other parameterized list. - ›Each element read from a
List<?>comes out asObject, which is enough sinceprintAllonly prints it. - ›Nothing can be added to a
List<?>(exceptnull) because the compiler cannot verify what type the unknown list actually holds — this is why the wildcard is safe for read-only methods.
Wildcard vs Type Parameter - Side by Side
| Aspect | Wildcard ? | Named Type Parameter <T> |
|---|---|---|
| Can reference the type by name | No — ? has no name to use | Yes — T can appear in return type, other params, body |
Call add() on List<?> | Only null — the element type is unknown | Yes — list.add(element) where element is T |
Read from List<?> | Yes — elements come out as Object | Yes — elements come out as T |
Accepts List<String> and List<Integer> at the same call site | Yes — ? matches any parameterized list | Only if declared as List<T> and called twice with different T |
| Syntax location | Inside <> where a type argument goes | After class/method name in <> declaration |
| Use for structural operations only (size, isEmpty) | Best choice — clean and explicit | Works but over-specified |
| Use when type appears in return type | Cannot — no name to return | Required |
Best Practices
Use Collection<?> instead of List<?> when the method does not care about ordering or index access. A method that only counts or prints elements should accept Collection<?> rather than List<?> — it is a broader, more flexible type that still prevents mutation of the element type.
Prefer ? over <T> for method parameters when the type is genuinely not needed. A printAll(List<?> items) is clearer in intent than <T> void printAll(List<T> items) — the wildcard version explicitly signals "I do not use the element type for anything meaningful." When a reviewer reads List<?>, they immediately know the method is read-only with respect to the element type.
Do not confuse List<?> with List<Object>. List<Object> accepts only an actual List<Object> — almost nothing in a typed codebase is List<Object>. List<?> accepts any parameterized list. When writing a utility method that should work with any list, List<?> is almost always the right signature.
Reserve wildcard capture (the public-? / private-<T> pattern) for algorithms that must mutate the list. If the method only reads, the public List<?> signature needs no private helper. The helper pattern is specifically for cases like reverse and swap where elements must be re-inserted — operations that require naming the unknown type internally.
Common Mistakes
Mistake 1 - Trying to Add Elements to a List<?>
import java.util.ArrayList;
import java.util.List;
// WRONG - the compiler rejects add() on List<?> because it cannot
// verify that the argument type matches the unknown element type.
// This is true even if the argument is the "right" type logically.
static void addToWildcard(List<?> list) {
// list.add("something"); // COMPILE ERROR
// "no suitable method found for add(String)"
// list.add(42); // COMPILE ERROR - same reason
list.add(null); // this compiles - null is always type-safe
}
// CORRECT OPTION A - if elements need to be added, use a named type parameter
static <T> void addElement(List<T> list, T element) {
list.add(element); // compiler knows element is T - safe
}
// CORRECT OPTION B - if the method only reads, the wildcard is correct
// and the add() attempts are the mistake, not the wildcard
static void readFromWildcard(List<?> list) {
for (Object item : list) { // reading is fine - elements come out as Object
System.out.println(item);
}
}