Skip to main content

Section 10.10 Optional: Wildcard Type Parameters

We’ve seen how generic types like List<String> provide compile-time safety, ensuring a list intended for strings only holds strings. We also saw how bounded types like <T extends Number> allow generic methods to work with types having specific capabilities.
However, this specificity can sometimes be too restrictive. Consider a simple method designed to print all elements in any list:
import java.util.List;
import java.util.Arrays; // Needed for Arrays.asList

public class ListPrinterProblem {
    // This works ONLY for List<Object>
    public static void printListObjects(List<Object> list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob");
        List<Integer> numbers = Arrays.asList(1, 2, 3);

        // printListObjects(names);   // Compile Error! Why?
        // printListObjects(numbers); // Compile Error! Why?
    }
}
Why do these calls fail? It’s because of a key rule in generics: even though String is a subtype of Object, a List<String> is not considered a subtype of List<Object>. This property is called invariance, meaning that generic types parameterized by different type arguments generally have no subtype relationship, even if their type arguments do. Let’s see why this restriction is crucial for type safety.

Note 10.10.1. Why Generics are Invariant (Type Safety).

Imagine if Java did allow assigning a List<String> to a List<Object>. The following unsafe scenario could occur:
List<String> strings = new java.util.ArrayList<>();
strings.add("Safe");

// Imagine this assignment was allowed (it is NOT in Java):
// List<Object> objects = strings; // Hypothetically allowed

// Now, using the 'objects' reference, we could add non-Strings:
// objects.add(Integer.valueOf(42)); // This would corrupt the list 'strings'!

// Later, trying to get an element assuming it's a String would fail:
// String s = strings.get(1); // Runtime ClassCastException! Would crash here.
To prevent this exact kind of type corruption, generic types like List<T> are invariant by default. This ensures compile-time safety, preventing the kind of runtime exceptions (ClassCastException) that could occur if a list of strings were accidentally treated as a list capable of holding any object.
So, how do we write a method like printList that can accept a list of anything safely? Or a method that can process a list containing any kind of Number? This is where wildcards (?) come in. Wildcards represent an unknown type and allow us to create more flexible method signatures that can accept a wider range of generic type instantiations while maintaining defined safety rules.

Subsection 10.10.1 Unbounded Wildcards (?)

The simplest wildcard is the unbounded wildcard, represented by a question mark (?). It means "a list (or other generic type) of some unknown type".
Let’s rewrite our printList method using an unbounded wildcard:
import java.util.List;
import java.util.Arrays;

public class ListPrinter {
    // This method accepts a List of ANY type
    public static void printAnyList(List<?> list) {
        System.out.print("[");
        for (int i = 0; i < list.size(); i++) {
            // We can safely get elements, but only as Object
            Object elem = list.get(i);
            System.out.print(elem);
            if (i < list.size() - 1) {
                System.out.print(", ");
            }
        }
        System.out.println("]");

        // Limitations: Cannot safely add elements (except null)
        // list.add("new element"); // Compile Error! Can't add String to List<?>
                                  // The compiler doesn't know if '?' is String or a supertype.
        // list.add(null); // This is allowed (see note below)
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob");
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        List<Object> objects = Arrays.asList("Mixed", 42, true);
        // List<Player> players = Arrays.asList(new Player("P1")); // Assuming Player exists

        printAnyList(names);   // Now OK!
        printAnyList(numbers); // Now OK!
        printAnyList(objects); // OK
        // printAnyList(players); // OK
    }
}
Using List<?> allows the method to accept lists parameterized with any type. Inside the method, however, the compiler only knows that the list holds objects of "some unknown type". Therefore:
  • You can safely read elements, but they are treated as type Object.
  • You can call methods that don’t depend on the type parameter (like size(), isEmpty(), clear()).
  • You cannot safely add elements to the list (except null), because the compiler doesn’t know the exact type that ? represents. Adding anything other than null could violate the type safety of the original list (e.g., adding an Integer to what might be a List<String>).

Note 10.10.2. Why is Adding null Allowed?

You might wonder why adding null is permitted for List<?> when other additions are not. This is because the value null is considered compatible with any reference type in Java. Since ? represents some unknown reference type, adding null doesn’t violate type safety, although it’s often not practically useful in these scenarios.
Unbounded wildcards are useful when the method’s logic works entirely with methods from Object or methods of the generic type itself that don’t involve the type parameter (like size()).

Note 10.10.3. List<Object> vs. List<?>.

It’s important to distinguish these two. A List<Object> is a list that can specifically hold objects of any type (because all classes extend Object). You can add any object to a List<Object>. However, as we saw, a List<String> cannot be assigned to a List<Object> variable.
A List<?>, on the other hand, represents a list of a fixed but unknown type. You can assign a List<String> or a List<Integer> to a List<?> variable, but you cannot safely add elements to it because the specific type is unknown to the compiler. List<?> offers more flexibility for method parameters that read from lists, while List<Object> offers flexibility for storing diverse types but requires casting on retrieval.

Subsection 10.10.2 Upper Bounded Wildcards (? extends Type)

Sometimes, we need a method to accept lists of various subtypes, but we also need to call methods specific to a common superclass or interface of those subtypes. For example, calculating the sum of a list containing any kind of Number (Integer, Double, etc.). An unbounded wildcard List<?> isn’t enough, because retrieving elements only gives us Object, which doesn’t have methods like doubleValue().
This is where upper bounded wildcards come in. The syntax ? extends Type means "an unknown type that is either Type or a subtype of Type". This is useful when the method needs to read items from the generic structure and treat them as at least type Type.
import java.util.List;
import java.util.Arrays;

public class Calculator {
    // Accepts a List of Number or any subclass (Integer, Double, Float, etc.)
    public static double sumList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            // Safe to get elements and treat them as Number (the bound)
            // because we know ? is guaranteed to be at least a Number.
            sum += num.doubleValue(); // Can safely call Number methods
        }
        // Cannot safely add elements (compiler doesn't know exact subtype)
        // list.add(Integer.valueOf(5)); // Compile Error! Cannot add Integer to List<? extends Number>
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> ints = Arrays.asList(1, 2, 3);
        List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
        List<String> strings = Arrays.asList("a", "b"); // Cannot use String list here

        System.out.println("Sum of ints: " + sumList(ints));     // OK! Integer extends Number
        System.out.println("Sum of doubles: " + sumList(doubles)); // OK! Double extends Number
        // System.out.println(sumList(strings)); // Compile Error! String doesn't extend Number
    }
}
The signature List<? extends Number> allows the sumList method to accept both List<Integer> and List<Double> (and lists of any other Number subtype). Inside the method:
  • You can safely read elements and treat them as the bound type (Number in this case), allowing you to call methods defined in Number (like doubleValue()).
  • You still cannot safely add elements (except null) because the compiler doesn’t know if the list is specifically a List<Integer> or a List<Double>, etc. Adding an Integer to a List<Double> would violate type safety.
Rule of thumb: Use an upper bounded wildcard (? extends Type) when your method needs to read elements from a generic structure and treat them as type Type.

Subsection 10.10.3 Lower Bounded Wildcards (? super Type)

Conversely, sometimes you need a method that can add elements of a specific type to a list, where the list itself might be parameterized with that specific type or any of its supertypes. For example, adding Integers to a List<Integer>, a List<Number>, or even a List<Object>. We need flexibility in what kind of list we pass in, as long as it can safely accept Integers.
This is achieved using lower bounded wildcards. The syntax ? super Type means "an unknown type that is either Type or a supertype of Type". This is useful when the method needs to write items of type Type into the generic structure.
import java.util.List;
import java.util.ArrayList;

public class ListAdder {

    // Accepts a List of Integer or any supertype (Number, Object)
    public static void addNumbersToList(List<? super Integer> list) {
        // It's safe to ADD Integers (or subtypes like int via autoboxing)
        // because any list that can hold Integer or its supertypes
        // (Number, Object) can definitely hold an Integer.
        list.add(10);
        list.add(20);
        list.add(Integer.valueOf(30));

        // Reading elements is limited - compiler only guarantees Object
        // Object obj = list.get(0); // This is safe
        // Integer i = list.get(0); // Compile Error! Might not be an Integer (could be Number or Object).
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();
        List<Double> doubleList = new ArrayList<>(); // Double is not a supertype of Integer

        addNumbersToList(integerList); // OK
        addNumbersToList(numberList);  // OK
        addNumbersToList(objectList);  // OK
        // addNumbersToList(doubleList); // Compile Error! List<Double> cannot safely accept Integer

        System.out.println("integerList: " + integerList);
        System.out.println("numberList: " + numberList);
        System.out.println("objectList: " + objectList);
    }
}
The signature List<? super Integer> allows addNumbersToList to accept lists that are guaranteed to be able to hold Integer objects. Inside the method:
  • You can safely add objects of type Integer (or its subtypes, though none exist) to the list.
  • You cannot safely read elements expecting a specific type other than Object, because the list might be a List<Number> or List<Object>, and retrieving an element only guarantees it’s an Object.
Rule of thumb: Use a lower bounded wildcard (? super Type) when your method needs to write objects of type Type into a generic structure.

Subsection 10.10.4 Summary: Wildcards

Wildcards (?) provide essential flexibility when designing methods that accept generic types as parameters, allowing them to work with a range of related generic instantiations instead of just one specific type argument.
  • <?> (Unbounded): Accepts any type argument. Useful when the code only relies on Object methods or methods independent of the type parameter. Reading yields Object; writing is generally disallowed (except null).
  • <? extends Type> (Upper Bound): Accepts Type or any subtype. Use when you need to read elements as type Type. Writing is disallowed (except null).
  • <? super Type> (Lower Bound): Accepts Type or any supertype. Use when you need to write elements of type Type. Reading only guarantees Object.
  • If a method needs to reliably both read and write elements using their specific generic type, use a named type parameter (e.g., <T>) instead of a wildcard. (See note below).
Wildcards are a crucial part of leveraging generics effectively, particularly when designing flexible APIs and utility methods that work with various generic collections or containers. Understanding how they interact with type safety is essential. Now that we’ve seen how generics work on the surface, let’s briefly look at how Java implements them internally through a process called type erasure.

Note 10.10.4. Advanced: When Wildcards Aren’t Enough (Reading and Writing).

What if your method needs to both reliably read elements as a specific type and write elements of that same type? In such cases, wildcards usually don’t work because neither extends nor super allows both operations safely. For these situations, you typically need a specific type parameter, often declared on the method itself (making it a generic method).
Consider a method to swap the first two elements of a list:
import java.util.List;
import java.util.Arrays;

public class ListSwapper {
    // Needs to get elements as T AND write (set) elements as T.
    // Therefore, uses a type parameter T, not a wildcard.
    public static <T> void swapFirstTwo(List<T> list) {
        if (list == null || list.size() < 2) {
            return; // Cannot swap if list has fewer than 2 elements
        }
        T first = list.get(0);  // Read as T
        T second = list.get(1); // Read as T
        list.set(0, second);    // Write T
        list.set(1, first);     // Write T
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("First", "Second", "Third");
        // Note: Arrays.asList returns a fixed-size list. To modify it:
        List<String> mutableNames = new java.util.ArrayList<>(names);

        System.out.println("Before swap: " + mutableNames);
        swapFirstTwo(mutableNames); // OK, compiler infers T is String
        System.out.println("After swap: " + mutableNames);

        // swapFirstTwo(List<? extends String> list) // Wouldn't compile: list.set is disallowed
        // swapFirstTwo(List<? super String> list) // Wouldn't compile: list.get returns Object
    }
}
Here, we declare a generic method <T> void swapFirstTwo(List<T> list). Using the type parameter T allows the method to safely get elements (knowing they are type T) and safely set elements (knowing the list expects type T). Using wildcards like List<? extends T> or List<? super T> would prevent either the get or the set operation from being type-safe.

Exercises 10.10.5 Exercises

1.

Why is a List<String> not a subtype of List<Object> in Java?
  • Because type erasure prevents the detection of element types at runtime.
  • While type erasure is a mechanism in Java generics implementation, it’s not the primary reason for this invariance property.
  • For type safety, to prevent unsafe operations like adding an Integer to what is actually a List of Strings.
  • Correct! If a List<String> were allowed to be assigned to a List<Object> variable, it would be possible to add non-String objects to it, which would break type safety when the list is used elsewhere expecting only Strings.
  • Because the Object class cannot represent the behaviors of its subclasses.
  • This is not the reason. In fact, an Object reference can point to any subclass instance, but this is a different concept from generic type relationships.
  • Due to limitations in the JVM architecture.
  • The JVM architecture is not the limiting factor here. This is a deliberate design choice in the Java type system to ensure type safety.

2.

Which of the following is true about operations on a variable of type List<?>?
  • You can add any object to it.
  • This is incorrect. You cannot add objects (except null) to a List<?> because the specific type is unknown and adding arbitrary objects would violate type safety.
  • You can add elements of the same type as those already in the list.
  • Even if you can determine the runtime type of elements in the list, the compiler still doesn’t know the type parameter, so you cannot add elements.
  • You can only retrieve elements as Objects, and cannot add elements (except null).
  • Correct! With List<?>, elements can only be retrieved as Objects, and adding elements (except null) is not allowed since the compiler cannot verify type safety.
  • You cannot perform any operations on the list at all.
  • This is not true. You can still call methods like size(), isEmpty(), get() (returning Object), etc. on a List<?>.

3.

A method with the signature void processElements(List<? extends Number> list) can:
  • Add any Number subclass (like Integer or Double) to the list.
  • This is not safe. Even though the list contains Number subtypes, the compiler doesn’t know the exact type (could be List<Integer> or List<Double>), so adding is not allowed.
  • Safely read elements as Number objects and call Number methods on them.
  • Correct! When using <? extends Number>, you know that each element is at least a Number, so you can safely read elements and use them as Number instances.
  • Only work with lists that contain exactly Number objects, not subclasses.
  • The wildcard <? extends Number> actually means the list can contain Number or any of its subclasses like Integer, Double, etc.
  • Cast each element to any Number subclass without checking.
  • This would be unsafe. Even though elements are guaranteed to be Number or a subclass, you don’t know which specific subclass, so arbitrary casting is not safe.

4.

What is the purpose of a lower bounded wildcard like <? super Integer>?
  • To allow the list to hold only subtypes of Integer.
  • This is incorrect. Integer has no subtypes, and <? super Integer> would allow Integer or any of its supertypes.
  • To ensure you can safely read elements as Integer objects.
  • This is not the primary purpose. With <? super Integer>, you can only read elements as Objects, not as Integers specifically.
  • To allow safely adding Integer objects to the collection.
  • Correct! <? super Integer> means the list can hold Integer or any supertype, so it’s safe to add Integer objects to it.
  • To permit numeric operations on all elements in the list.
  • This is not correct. <? super Integer> does not guarantee that all elements have numeric operations, as they could be Object instances.

5.

When should you use a type parameter <T> instead of a wildcard <?>?
  • Always, as type parameters are always more flexible than wildcards.
  • This is not true. Each has their use cases, and wildcards are sometimes more appropriate.
  • Only when dealing with collections.
  • The choice between wildcards and type parameters is not determined by whether you’re working with collections.
  • When you need to ensure type safety at runtime.
  • Due to type erasure, neither type parameters nor wildcards provide runtime type safety beyond what normal Java classes offer.
  • When you need to both read elements as a specific type and write elements of that same type.
  • Correct! Type parameters are appropriate when you need to both read and write with the same specific type, which wildcards don’t easily allow.
You have attempted of activities on this page.