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.

1// File: InvarianceProblemDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class InvarianceProblemDemo { 7 8 // A method that prints every element. 9 // Instinct says: "accept List<Object> - everything IS an Object" 10 static void printAllBroken(List<Object> items) { 11 for (Object item : items) { 12 System.out.println(" " + item); 13 } 14 } 15 16 // The wildcard fix: List<?> accepts a list of ANY element type 17 static void printAll(List<?> items) { 18 for (Object item : items) { // element type is unknown - comes out as Object 19 System.out.println(" " + item); 20 } 21 } 22 23 public static void main(String[] args) { 24 25 List<String> productNames = List.of("Wireless Mouse", "Laptop Stand", "USB Hub"); 26 List<Integer> stockCounts = List.of(120, 45, 200); 27 List<Double> prices = List.of(799.0, 1299.0, 499.0); 28 29 System.out.println("=== printAll(List<?>) - accepts any List ==="); 30 printAll(productNames); // List<String> passed as List<?> - works 31 System.out.println(" ---"); 32 printAll(stockCounts); // List<Integer> passed as List<?> - works 33 System.out.println(" ---"); 34 printAll(prices); // List<Double> passed as List<?> - works 35 36 System.out.println(); 37 38 System.out.println("=== The List<Object> approach fails at compile time ==="); 39 // printAllBroken(productNames); // COMPILE ERROR: 40 // "incompatible types: List<String> cannot be converted to List<Object>" 41 // List<String> is NOT a List<Object> even though String IS an Object 42 // Generic types are INVARIANT - the subtype relationship on elements 43 // does NOT transfer to the parameterized type 44 45 // Only an actual List<Object> works with List<Object> parameter 46 List<Object> mixed = new ArrayList<>(); 47 mixed.add("A String"); 48 mixed.add(42); 49 printAllBroken(mixed); // compiles - this really is List<Object> 50 51 System.out.println(); 52 53 System.out.println("=== What you CAN do with List<?> ==="); 54 List<String> sample = new ArrayList<>(List.of("Swiggy", "Zomato", "Blinkit")); 55 List<?> wildcard = sample; // assigning List<String> to List<?> - legal 56 57 System.out.println("size() : " + wildcard.size()); 58 System.out.println("isEmpty() : " + wildcard.isEmpty()); 59 System.out.println("contains : " + wildcard.contains("Swiggy")); 60 61 Object first = wildcard.get(0); // returns Object - no type known 62 System.out.println("get(0) : " + first); 63 64 // wildcard.add("New Item"); // COMPILE ERROR 65 // "no suitable method found for add(String)" 66 // The compiler cannot verify "New Item" is the right type 67 wildcard.add(null); // null is the one exception - always safe 68 System.out.println("add(null) : succeeded (null is always safe)"); 69 } 70}
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>.

1// File: ReadOnlyWildcardDemo.java 2 3import java.util.*; 4 5public class ReadOnlyWildcardDemo { 6 7 // Counts how many elements satisfy a condition. 8 // Collection<?> accepts Collection<String>, Collection<Integer>, etc. 9 static int countNonNull(Collection<?> items) { 10 int count = 0; 11 for (Object item : items) { 12 if (item != null) count++; 13 } 14 return count; 15 } 16 17 // Returns a formatted summary line for any list. 18 static String summarize(List<?> items) { 19 if (items.isEmpty()) return "Empty list"; 20 Object first = items.get(0); 21 Object last = items.get(items.size() - 1); 22 return "List[size=" + items.size() 23 + ", first=" + first 24 + ", last=" + last + "]"; 25 } 26 27 // Checks if two lists have the same size. 28 // Both can be lists of completely different element types. 29 static boolean sameSizeAs(List<?> first, List<?> second) { 30 return first.size() == second.size(); 31 } 32 33 // Prints a map's contents where keys and values can be any types. 34 static void printMap(Map<?, ?> map) { 35 map.forEach((key, value) -> 36 System.out.println(" " + key + " -> " + value)); 37 } 38 39 public static void main(String[] args) { 40 41 List<String> cities = List.of("Mumbai", "Delhi", "Bengaluru"); 42 List<Integer> codes = List.of(400001, 110001, 560001); 43 List<Double> rates = List.of(7.5, 8.1, null, 6.9); 44 45 System.out.println("=== countNonNull ==="); 46 System.out.println("cities non-null: " + countNonNull(cities)); 47 System.out.println("rates non-null : " + countNonNull(rates)); 48 49 System.out.println(); 50 51 System.out.println("=== summarize ==="); 52 System.out.println(summarize(cities)); 53 System.out.println(summarize(codes)); 54 System.out.println(summarize(List.of())); 55 56 System.out.println(); 57 58 System.out.println("=== sameSizeAs ==="); 59 System.out.println("cities and codes same size? " + sameSizeAs(cities, codes)); 60 System.out.println("cities and rates same size? " + sameSizeAs(cities, rates)); 61 62 System.out.println(); 63 64 System.out.println("=== printMap with Map<String, Integer> ==="); 65 Map<String, Integer> stockMap = new LinkedHashMap<>(); 66 stockMap.put("Wireless Mouse", 120); 67 stockMap.put("Laptop Stand", 45); 68 stockMap.put("USB Hub", 200); 69 printMap(stockMap); 70 71 System.out.println("=== printMap with Map<Integer, String> ==="); 72 Map<Integer, String> idToCity = Map.of(1, "Mumbai", 2, "Pune", 3, "Nashik"); 73 printMap(idToCity); 74 } 75}
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.

1// File: ClassWildcardDemo.java 2 3import java.lang.reflect.Method; 4 5public class ClassWildcardDemo { 6 7 static class InventoryService { 8 public void updateStock(String productId, int quantity) {} 9 public int getStock(String productId) { return 0; } 10 private void reconcileInternal() {} 11 } 12 13 // Accepts Class<?> - works for any class, not just a specific one. 14 // Used in frameworks and DI containers that inspect arbitrary classes. 15 static void describeClass(Class<?> clazz) { 16 System.out.println("Class : " + clazz.getSimpleName()); 17 System.out.println("Package : " + clazz.getPackageName()); 18 System.out.println("Interface?: " + clazz.isInterface()); 19 20 Method[] methods = clazz.getDeclaredMethods(); 21 System.out.println("Methods : " + methods.length); 22 for (Method method : methods) { 23 System.out.println(" -> " + method.getName() 24 + " (" + method.getParameterCount() + " params)"); 25 } 26 } 27 28 // Creates an instance of any no-arg class using reflection. 29 // The return type Object is all that can be offered - the actual 30 // type is unknown, which is exactly why Class<?> is used. 31 static Object createInstance(Class<?> clazz) throws Exception { 32 return clazz.getDeclaredConstructor().newInstance(); 33 } 34 35 public static void main(String[] args) throws Exception { 36 37 System.out.println("=== describeClass with InventoryService ==="); 38 describeClass(InventoryService.class); 39 40 System.out.println(); 41 42 System.out.println("=== describeClass with String (from the JDK) ==="); 43 // Class<?> - works for any class, whether ours or JDK's 44 Class<?> stringClass = String.class; 45 System.out.println("Class : " + stringClass.getSimpleName()); 46 System.out.println("Is final : " + java.lang.reflect.Modifier.isFinal( 47 stringClass.getModifiers())); 48 49 System.out.println(); 50 51 System.out.println("=== createInstance ==="); 52 Object instance = createInstance(StringBuilder.class); 53 System.out.println("Created : " + instance.getClass().getSimpleName()); 54 System.out.println("Instance : " + instance); 55 } 56}
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.

1// File: WildcardCaptureDemo.java 2 3import java.util.List; 4 5public class WildcardCaptureDemo { 6 7 // PUBLIC API: accepts any List<?> - the unknown type is captured 8 // and passed to the helper where it can be named 9 public static void reverse(List<?> list) { 10 reverseHelper(list); // compiler captures ? as a specific type T 11 } 12 13 // PRIVATE HELPER: the captured type is given a name <T> 14 // This is the only way to write algorithms that need to name the type 15 // of elements in a wildcard list 16 private static <T> void reverseHelper(List<T> list) { 17 int left = 0; 18 int right = list.size() - 1; 19 while (left < right) { 20 T temp = list.get(left); 21 list.set(left, list.get(right)); 22 list.set(right, temp); 23 left++; 24 right--; 25 } 26 } 27 28 // Another example: swap two elements in a wildcard list 29 public static void swap(List<?> list, int i, int j) { 30 swapHelper(list, i, j); 31 } 32 33 private static <T> void swapHelper(List<T> list, int i, int j) { 34 T temp = list.get(i); 35 list.set(i, list.get(j)); 36 list.set(j, temp); 37 } 38 39 public static void main(String[] args) { 40 java.util.List<String> cities = new java.util.ArrayList<>( 41 java.util.List.of("Mumbai", "Pune", "Nashik", "Aurangabad")); 42 43 System.out.println("Before reverse: " + cities); 44 reverse(cities); // List<String> passed as List<?> - capture happens 45 System.out.println("After reverse : " + cities); 46 47 System.out.println(); 48 49 java.util.List<Integer> amounts = new java.util.ArrayList<>( 50 java.util.List.of(500, 1200, 800, 3000)); 51 52 System.out.println("Before swap: " + amounts); 53 swap(amounts, 0, 3); // List<Integer> passed as List<?> - capture happens 54 System.out.println("After swap : " + amounts); 55 } 56}
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.

Real-World Example - CRED Analytics Dashboard

A financial app's analytics layer needs to display data from several different reporting sources — transaction counts, reward points, bill amounts, membership tiers — each stored as a different Java type. A shared rendering layer reads and displays any of these reports without caring about the element types. The wildcard lets utility methods in the rendering layer accept all report containers without duplicating code per type.

1// File: ReportSection.java 2 3import java.util.List; 4 5// A report section groups a label with a list of items of some type. 6// The element type is stored as an erased wildcard because the rendering 7// layer never needs to know WHAT type the items are - only their string 8// representation for display. 9public class ReportSection { 10 11 private final String title; 12 private final List<?> items; 13 private final String unit; 14 15 public ReportSection(String title, List<?> items, String unit) { 16 this.title = title; 17 this.items = List.copyOf(items); 18 this.unit = unit; 19 } 20 21 public String getTitle() { return title; } 22 public List<?> getItems() { return items; } // List<?> - type is unknown to callers 23 public String getUnit() { return unit; } 24 public int itemCount() { return items.size(); } 25}
1// File: DashboardRenderer.java 2 3import java.util.List; 4 5public class DashboardRenderer { 6 7 // Renders any ReportSection regardless of its item type. 8 // ? is the right choice here - the renderer only reads each item 9 // via toString() (called implicitly by concatenation/println). 10 public static void renderSection(ReportSection section) { 11 System.out.println("+-----------------------------------------+"); 12 System.out.printf("| %-39s |%n", section.getTitle()); 13 System.out.println("+-----------------------------------------+"); 14 15 List<?> items = section.getItems(); 16 if (items.isEmpty()) { 17 System.out.println("| (no data) |"); 18 } else { 19 for (int i = 0; i < items.size(); i++) { 20 String formatted = String.format(" %d. %s %s", 21 i + 1, items.get(i), section.getUnit()); 22 System.out.printf("| %-39s |%n", formatted); 23 } 24 } 25 26 System.out.println("+-----------------------------------------+"); 27 System.out.println(); 28 } 29 30 // A utility that checks whether any section in a dashboard is empty. 31 // List<ReportSection> contains sections, each with a List<?> inside. 32 // The outer List is concrete; the inner items are wildcarded. 33 public static boolean hasEmptySection(List<ReportSection> sections) { 34 for (ReportSection section : sections) { 35 if (section.getItems().isEmpty()) return true; 36 } 37 return false; 38 } 39 40 // Counts the total number of data points across all sections. 41 // Again, only structural operations on List<?> - size() - no type needed. 42 public static int totalDataPoints(List<ReportSection> sections) { 43 int total = 0; 44 for (ReportSection section : sections) { 45 total += section.itemCount(); 46 } 47 return total; 48 } 49}
1// File: CredDashboardDemo.java 2 3import java.util.List; 4 5public class CredDashboardDemo { 6 7 public static void main(String[] args) { 8 9 // Each section holds a different element type. 10 // ReportSection stores them all as List<?> - the renderer does not care. 11 ReportSection transactions = new ReportSection( 12 "Recent Transactions", 13 List.of("HDFC CC Bill - Rs.4200", "Zepto - Rs.849", "Swiggy - Rs.412"), 14 "" 15 ); 16 17 ReportSection rewardPoints = new ReportSection( 18 "Reward Points (Last 3 Months)", 19 List.of(2450, 1890, 3100), // Integer items 20 "pts" 21 ); 22 23 ReportSection billAmounts = new ReportSection( 24 "Upcoming Bills", 25 List.of(4200.0, 1500.0, 800.0), // Double items 26 "Rs." 27 ); 28 29 ReportSection membershipTiers = new ReportSection( 30 "Membership Status", 31 List.of("CRED Black", "Travel Benefits Active", "Lounge Access: 4 remaining"), 32 "" 33 ); 34 35 ReportSection emptySection = new ReportSection( 36 "Investment Nudges", 37 List.of(), // empty 38 "" 39 ); 40 41 List<ReportSection> dashboard = List.of( 42 transactions, rewardPoints, billAmounts, membershipTiers, emptySection); 43 44 System.out.println("=== CRED Analytics Dashboard ==="); 45 System.out.println(); 46 47 // renderSection accepts any ReportSection regardless of its item type 48 for (ReportSection section : dashboard) { 49 DashboardRenderer.renderSection(section); 50 } 51 52 System.out.println("=== Dashboard summary ==="); 53 System.out.println("Has empty section : " + 54 DashboardRenderer.hasEmptySection(dashboard)); 55 System.out.println("Total data points : " + 56 DashboardRenderer.totalDataPoints(dashboard)); 57 } 58}
Output:
=== CRED Analytics Dashboard ===

+-----------------------------------------+
| Recent Transactions                     |
+-----------------------------------------+
|   1. HDFC CC Bill - Rs.4200             |
|   2. Zepto - Rs.849                     |
|   3. Swiggy - Rs.412                    |
+-----------------------------------------+

+-----------------------------------------+
| Reward Points (Last 3 Months)           |
+-----------------------------------------+
|   1. 2450 pts                           |
|   2. 1890 pts                           |
|   3. 3100 pts                           |
+-----------------------------------------+

+-----------------------------------------+
| Upcoming Bills                          |
+-----------------------------------------+
|   1. 4200.0 Rs.                         |
|   2. 1500.0 Rs.                         |
|   3. 800.0 Rs.                          |
+-----------------------------------------+

+-----------------------------------------+
| Membership Status                       |
+-----------------------------------------+
|   1. CRED Black                         |
|   2. Travel Benefits Active             |
|   3. Lounge Access: 4 remaining         |
+-----------------------------------------+

+-----------------------------------------+
| Investment Nudges                       |
+-----------------------------------------+
|  (no data)                              |
+-----------------------------------------+

=== Dashboard summary ===
Has empty section : true
Total data points : 10

ReportSection stores items as List<?> because at the point where sections are created, the rendering layer should not need to know the element type — it only renders. renderSection() accepts any ReportSection and reads items through toString() (via string concatenation) — it never adds elements, never creates elements, never needs to call type-specific methods. hasEmptySection() and totalDataPoints() access only structural properties. Every operation on the wildcard list is read-only and structural, which is exactly the scope where ? belongs.

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<?>

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG - the compiler rejects add() on List<?> because it cannot 5// verify that the argument type matches the unknown element type. 6// This is true even if the argument is the "right" type logically. 7static void addToWildcard(List<?> list) { 8 // list.add("something"); // COMPILE ERROR 9 // "no suitable method found for add(String)" 10 11 // list.add(42); // COMPILE ERROR - same reason 12 13 list.add(null); // this compiles - null is always type-safe 14} 15 16// CORRECT OPTION A - if elements need to be added, use a named type parameter 17static <T> void addElement(List<T> list, T element) { 18 list.add(element); // compiler knows element is T - safe 19} 20 21// CORRECT OPTION B - if the method only reads, the wildcard is correct 22// and the add() attempts are the mistake, not the wildcard 23static void readFromWildcard(List<?> list) { 24 for (Object item : list) { // reading is fine - elements come out as Object 25 System.out.println(item); 26 } 27}

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

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

1import java.util.List; 2 3// WRONG - get() on List<?> returns Object, not the element type. 4// Casting it to a specific type is an unchecked cast - ClassCastException 5// at runtime if the actual element is a different type. 6static void uncheckedRead(List<?> items) { 7 // String first = (String) items.get(0); // compiles but runtime risk 8 // If items is actually a List<Integer>, this cast throws ClassCastException 9} 10 11// CORRECT - when only Object-level operations are needed, keep the 12// result typed as Object. When type-specific operations are needed, 13// use a named type parameter instead of a wildcard. 14static void safeRead(List<?> items) { 15 Object first = items.get(0); // Object - safe, no cast 16 System.out.println(first.toString()); // Object methods are available 17} 18 19// If String-specific operations are needed, use a named parameter: 20static void stringSpecific(List<String> items) { 21 String first = items.get(0); // String - type is known and safe 22 System.out.println(first.toUpperCase()); // String method available 23}

Mistake 4 - Using Wildcard When the Type Must Be Returned

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

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