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.
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.
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.
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 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>).
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()).
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.
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.
Subsection10.10.3Lower 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 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.
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).
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.
Note10.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).
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.
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.
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<?>.
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.