Java Tutorial
🔍

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:

  1. ›List<?> means "a list of some unknown type," so printAll accepts List<String>, List<Integer>, or any other parameterized list.
  2. ›Each element read from a List<?> comes out as Object, which is enough since printAll only prints it.
  3. ›Nothing can be added to a List<?> (except null) 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

AspectWildcard ?Named Type Parameter <T>
Can reference the type by nameNo — ? has no name to useYes — T can appear in return type, other params, body
Call add() on List<?>Only null — the element type is unknownYes — list.add(element) where element is T
Read from List<?>Yes — elements come out as ObjectYes — elements come out as T
Accepts List<String> and List<Integer> at the same call siteYes — ? matches any parameterized listOnly if declared as List<T> and called twice with different T
Syntax locationInside <> where a type argument goesAfter class/method name in <> declaration
Use for structural operations only (size, isEmpty)Best choice — clean and explicitWorks but over-specified
Use when type appears in return typeCannot — no name to returnRequired

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); } }

Mistake 2 - Using List Where List<?> Is Needed
import java.util.List; // WRONG - accepts ONLY List<Object>. In a codebase that uses generics // correctly, List<Object> is almost never created. This method // is unusable with List<String>, List<Integer>, or any typed list. static void logItemsBroken(List<Object> items) { items.forEach(item -> System.out.println("[LOG] " + item)); } // Usage - the problem is visible here: List<String> names = List.of("Ananya", "Rahul"); // logItemsBroken(names); // COMPILE ERROR // "incompatible types: List<String> cannot be converted to List<Object>" // CORRECT - wildcard accepts any parameterized list static void logItems(List<?> items) { items.forEach(item -> System.out.println("[LOG] " + item)); } // Now all of these work: // logItems(names); // List<String> // logItems(List.of(1, 2, 3)); // List<Integer> // logItems(List.of(1.5, 2.5)); // List<Double>

Mistake 3 - Assigning the Result of get() to a Specific Type

import java.util.List; // WRONG - get() on List<?> returns Object, not the element type. // Casting it to a specific type is an unchecked cast - ClassCastException // at runtime if the actual element is a different type. static void uncheckedRead(List<?> items) { // String first = (String) items.get(0); // compiles but runtime risk // If items is actually a List<Integer>, this cast throws ClassCastException } // CORRECT - when only Object-level operations are needed, keep the // result typed as Object. When type-specific operations are needed, // use a named type parameter instead of a wildcard. static void safeRead(List<?> items) { Object first = items.get(0); // Object - safe, no cast System.out.println(first.toString()); // Object methods are available } // If String-specific operations are needed, use a named parameter: static void stringSpecific(List<String> items) { String first = items.get(0); // String - type is known and safe System.out.println(first.toUpperCase()); // String method available }

Mistake 4 - Using Wildcard When the Type Must Be Returned

import java.util.List; // WRONG - the return type cannot reference ? because ? has no name. // This does not even compile in a meaningful way - the return type // cannot use a wildcard from a method parameter. // The intent was "take the first element of any List and return it" // but List<?> cannot fulfill the return contract. static Object getFirstBroken(List<?> items) { return items.isEmpty() ? null : items.get(0); // Returns Object - caller gets Object and must cast, losing type safety } // CORRECT - when the return type must match the element type, // use a named type parameter so the relationship is expressed static <T> T getFirst(List<T> items, T defaultValue) { return items.isEmpty() ? defaultValue : items.get(0); // Returns T - caller gets the actual element type, no cast needed }

Interview Questions

Q1. What is the unbounded wildcard ? in Java generics, and what does List<?> mean?

The unbounded wildcard ? represents an unknown type — it is used in type arguments when the exact type is either not known or not needed. List<?> means "a list whose element type is unknown." It is the correct type to use when a method needs to accept lists of any parameterized type — List<String>, List<Integer>, List<Order> — without caring what the element type is. Reading from a List<?> yields Object references; adding to a List<?> is disallowed (except null) because the compiler cannot verify what type the unknown list accepts.

Q2. Why is List<?> different from List, and which should be used for a utility method?

List<Object> means the list specifically holds Object values — only an actual List<Object> can be passed where List<Object> is expected, because generic types are invariant. List<String> is not a List<Object> even though String is an Object. List<?> means the list holds some unknown type — any parameterized List can be passed where List<?> is expected. A utility method that reads from any list should declare List<?> as its parameter type, not List<Object>, because the wildcard version genuinely accepts any list while List<Object> accepts almost nothing in a well-typed codebase.

Q3. Why can you not add elements to a List<?>, and what is the one exception?

The compiler disallows list.add(element) on a List<?> because the element type is unknown — the compiler has no way to verify that element is the correct type for whatever the list actually holds at runtime. If the list is actually a List<String>, adding an Integer would corrupt it; since the compiler cannot see through the ?, it prohibits all additions as a conservative safety rule. The one exception is null, which is type-compatible with every reference type and is therefore always safe to add — it will never cause a type mismatch regardless of what the actual list holds.

Q4. What is wildcard capture, and why is the public-? / private-T pattern used?

Wildcard capture is the compiler's mechanism for treating a ? as a specific (but still unknown) type within a single call. When a List<?> is passed to a helper method declared as <T> void helper(List<T> list), the compiler "captures" the ? as T for that call, allowing the helper to perform operations like list.set(i, list.get(j)) that require knowing the element type. The public-? / private-<T> pattern is used when the public API should accept any list (best expressed with ?) but the algorithm must mutate the list (requires naming the type). The public method with ? delegates to the private method with <T>, and the compiler connects them through capture.

Q5. When should you use ? instead of a named type parameter T?

Use ? when the type appears in exactly one position and is never referenced again — typically when a method only reads from a collection (elements come out as Object), checks structural properties like size or emptiness, or works with Class<?> for reflection. Use <T> when the type must appear in two or more positions in the same declaration — in the return type and a parameter, in two parameters that must match, or when the method body needs to create instances or call type-specific methods on values of type T. The practical test: if the method body would compile and be correct with every element treated as Object, use ?. If the method needs to return or produce T values, use <T>.

Q6. Why does Java use invariant generic types, and how does the wildcard address the limitation?

Generic types are invariant — List<String> is not a subtype of List<Object> — because allowing covariance would break type safety. If List<String> were a List<Number>, a caller holding a List<Number> reference could call add(3.14) (a Double, which is a Number), corrupting what is actually a List<String>. The invariance rule prevents this by making the relationship between parameterized types independent of the relationship between their element types. The wildcard addresses the practical limitation that invariance creates: when a method genuinely should accept any list, List<?> expresses that safely — the ? allows any parameterized list to be passed, but the disabling of add() ensures the type integrity of the original list cannot be violated.

FAQs

Can you use ? as a method's return type?

Using ? directly as a type argument in a return type is legal syntactically — List<?> as a return type is valid — but it means the caller receives a List<?> and can only use its elements as Object. This is sometimes appropriate when a method genuinely cannot or should not commit to an element type. However, if the method could commit to a type, a named type parameter <T> producing List<T> is more useful for the caller. Methods that return List<?> are often storing or forwarding containers whose element type was lost at some API boundary.

What is the difference between ? and ? extends Object?

They are equivalent. List<?> and List<? extends Object> are the same type — both mean "a list of some unknown type that is Object or a subtype of Object," which describes every reference type in Java. Java treats them identically. The ? extends Object form is occasionally written explicitly to make the upper bound visible, but it adds no new information.

Can you nest wildcards, like List<List<?>>?

Yes. List<List<?>> means "a list of lists, where each inner list can have any element type." You can add a List<String> or a List<Integer> to a List<List<?>> without any cast. This is different from List<? extends List<?>> (an unknown list type whose elements are some kind of list). Nested wildcards appear in utility code that works with collections of collections.

Does using List<?> have any runtime overhead compared to List?

No. After type erasure, both List<?> and List<Object> compile to the same raw List in bytecode. The wildcard exists only at the source and compiler level — it imposes type-checking rules at compile time but produces identical bytecode. At runtime, there is no distinction between a List<String>, a List<Integer>, and a List<?> — all are just List.

Can you have a wildcard in a class declaration, like class Foo<?>?

No. Wildcards are type arguments and can only appear where type arguments go — inside <> when parameterizing a type. A class or method declaration uses type parameters (named variables like T, E, K), never wildcards. class Foo<?> is a compile error. Wildcards are for usage sites; named type parameters are for declaration sites.

Is List<?> the same as the raw type List?

No. A raw List is a List with no type information at all — it effectively works like pre-generics Java and produces unchecked warnings throughout. List<?> is a parameterized type with a wildcard argument — the compiler knows it is a generic List, knows the element type is unknown but consistent, and will still prevent adding non-null elements. They behave very similarly at the call site but convey different intentions: List<?> is intentional unknown-type-safe usage, while raw List is unintentional type erasure or legacy code.

Summary

The unbounded wildcard ? means "some type, which I do not know and do not need to name." Its primary purpose is breaking through generic invariance: List<String> cannot be passed where List<Object> is expected, but it can be passed where List<?> is expected. This makes ? the right choice for utility methods that only read from collections, check structural properties, or work with Class<?> in reflection code.

Two rules govern almost every wildcard decision. List<?> versus List<Object>: the wildcard accepts any parameterized list, List<Object> accepts only actual List<Object> instances — in a typed codebase, the wildcard is almost always the right choice for read-only utility parameters. ? versus <T>: the wildcard is for one-time, unnamed usage; the named type parameter is for anything where the type must be referenced again — in the return type, in another parameter, or to call type-specific methods inside the body.

The wildcard capture pattern ties these together: a public ?-based signature for the caller's convenience, delegating to a private <T> helper for the implementation's type naming. This is exactly how Collections.reverse(), Collections.sort(), and similar JDK methods are structured — clean public APIs built on wildcard-capture-enabled private implementations.

What to Read Next