Java Wildcard Types
Introduction
Java wildcard types are an important feature in Java's generics system that provide flexibility when working with generic types. They allow you to write code that can work with collections of different but related types, making your code more reusable and type-safe.
In this tutorial, we'll explore:
- What wildcards are and why we need them
- The three types of wildcards in Java
- How to use wildcards in practical scenarios
- Common pitfalls and best practices
What are Wildcards?
A wildcard is represented by the question mark (?
) symbol and indicates an unknown type. Wildcards are particularly useful when you want to work with generic classes in a more flexible way.
Consider this situation: you have a method that needs to work with lists of different types. Without wildcards, you would need to create separate methods for each specific type, leading to code duplication.
Types of Wildcards in Java
Java offers three types of wildcards:
- Unbounded wildcards:
List<?>
- Upper bounded wildcards:
List<? extends SuperClass>
- Lower bounded wildcards:
List<? super SubClass>
Let's explore each type in detail.
Unbounded Wildcards
An unbounded wildcard is represented as <?>
and means "a list of unknown type." It's useful when:
- The code can work with any type of object
- You're using methods that don't depend on the type parameter
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
Example:
List<Integer> integers = Arrays.asList(1, 2, 3);
List<String> strings = Arrays.asList("one", "two", "three");
printList(integers); // Output: 1 2 3
printList(strings); // Output: one two three
However, there's an important limitation: you cannot add elements to a collection using an unbounded wildcard (except for null
).
public static void addToList(List<?> list) {
// This will not compile!
// list.add("element"); // Compile-time error
// This is allowed
list.add(null);
}
Upper Bounded Wildcards
Upper bounded wildcards restrict the unknown type to a specific type or its subclasses. The syntax is <? extends Type>
. This is useful when you need to read from a generic collection.
public static double sumOfNumbers(List<? extends Number> numbers) {
double sum = 0.0;
for (Number num : numbers) {
sum += num.doubleValue();
}
return sum;
}
Example:
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
double intSum = sumOfNumbers(integers); // Works with Integer
double doubleSum = sumOfNumbers(doubles); // Works with Double
System.out.println("Sum of integers: " + intSum); // Output: Sum of integers: 6.0
System.out.println("Sum of doubles: " + doubleSum); // Output: Sum of doubles: 6.6
Similar to unbounded wildcards, you cannot add elements to a collection with an upper bounded wildcard (except for null
), as the compiler cannot guarantee type safety.
Lower Bounded Wildcards
Lower bounded wildcards restrict the unknown type to a specific type or its superclasses. The syntax is <? super Type>
. This is useful when you need to write to a generic collection.
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // This is allowed
}
}
Example:
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // Works because Number is a supertype of Integer
addNumbers(objects); // Works because Object is a supertype of Integer
System.out.println(numbers); // Output: [1, 2, 3, 4, 5]
System.out.println(objects); // Output: [1, 2, 3, 4, 5]
With lower bounded wildcards, you can add elements of the specified type or its subtypes to the collection. However, you can only read objects of type Object
from it:
public static void processElements(List<? super Integer> list) {
// We can add integers
list.add(10);
// We can only read as Object
Object obj = list.get(0);
// Integer n = list.get(0); // Compile-time error
}
PECS: Producer Extends, Consumer Super
There's a helpful mnemonic to remember when to use which wildcard: PECS - "Producer Extends, Consumer Super".
- Use
<? extends T>
when your collection is a producer (you read from it) - Use
<? super T>
when your collection is a consumer (you write to it)
// Producer: Collection produces (gives) values, so we use "extends"
public void copyElements(List<? extends T> source, List<T> destination) {
for (T item : source) {
destination.add(item); // Reading from source
}
}
// Consumer: Collection consumes (takes in) values, so we use "super"
public void addElements(List<? super T> destination, T... elements) {
for (T item : elements) {
destination.add(item); // Writing to destination
}
}
Real-World Example: Generic Method with Wildcards
Let's create a utility method that copies elements from one list to another:
public static <T> void copyList(List<? extends T> source, List<? super T> destination) {
for (T element : source) {
destination.add(element);
}
}
Example usage:
// Using class hierarchy: Animal -> Dog -> Poodle
class Animal {}
class Dog extends Animal {}
class Poodle extends Dog {}
public static void main(String[] args) {
List<Dog> dogs = Arrays.asList(new Dog(), new Dog());
// Destination can be of Dog type or any supertype of Dog
List<Animal> animals = new ArrayList<>();
List<Object> objects = new ArrayList<>();
copyList(dogs, animals); // Works: copying Dogs to Animals list
copyList(dogs, objects); // Works: copying Dogs to Objects list
// Source can be of Dog type or any subtype of Dog
List<Poodle> poodles = Arrays.asList(new Poodle(), new Poodle());
List<Dog> moreDogs = new ArrayList<>();
copyList(poodles, moreDogs); // Works: copying Poodles to Dogs list
System.out.println("Animals list size: " + animals.size()); // Output: Animals list size: 2
System.out.println("Objects list size: " + objects.size()); // Output: Objects list size: 2
System.out.println("More dogs list size: " + moreDogs.size()); // Output: More dogs list size: 2
}
Visualizing Wildcard Bounds
Here's a diagram showing how wildcard bounds work with a class hierarchy:
Common Mistakes and Best Practices
Mistake 1: Using wildcards unnecessarily
// Unnecessary use of wildcard
public static void printSize(List<?> list) {
System.out.println(list.size());
}
// Better approach - no wildcard needed
public static <T> void printSize(List<T> list) {
System.out.println(list.size());
}
Mistake 2: Using the wrong wildcard bound
// Trying to add elements to a "produces" collection
public static void addElements(List<? extends Number> list) {
// list.add(1); // Compile-time error!
}
// Correct approach
public static void addElements(List<? super Number> list) {
list.add(1);
list.add(2.0);
}
Best Practice: Use wildcards in API signatures, not in variables
// In API (method signatures)
public void processElements(List<? extends Number> numbers) { /* ... */ }
// In variable declarations, use concrete types when possible
List<Integer> integers = new ArrayList<>();
// Instead of: List<? extends Number> integers = new ArrayList<Integer>();
Summary
Java wildcards are a powerful feature that allows for more flexible generic programming:
- Unbounded wildcards (
<?>
) allow you to work with collections of any type - Upper bounded wildcards (
<? extends T>
) allow you to read from collections of a specific type or its subtypes - Upper bounded wildcards are useful for producers (you get data from them)
- Lower bounded wildcards (
<? super T>
) allow you to write to collections of a specific type or its supertypes - Lower bounded wildcards are useful for consumers (you put data into them)
- Remember PECS: "Producer Extends, Consumer Super"
By understanding when and how to use wildcards, you can write more flexible and type-safe generic code.
Exercises
- Write a method that finds the maximum value in a list of comparable objects using a wildcard.
- Create a method that copies elements from a list of any type to a list of strings by calling the
toString()
method on each element. - Write a utility method that merges two sorted lists into a third sorted list using wildcards.
- Implement a method that checks if two lists contain the same elements in the same order, regardless of their generic types.
Additional Resources
- Oracle's Java Tutorials: Generics
- Effective Java by Joshua Bloch - Item 31: Use bounded wildcards to increase API flexibility
- Java Generics and Collections by Maurice Naftalin and Philip Wadler
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)