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