Java Generics: Wildcards Explained
Java Generics: Wildcards Explained
Wildcards (?) in Java generics provide flexibility when working with unknown or varied types. They are primarily used in method parameters to accept a range of related types while maintaining type safety.
1. Upper-Bounded Wildcard (? extends T)
- Purpose: Accept a type T or any of its subtypes (read-only operations).
- Use Case: When you need to read data from a generic structure.
Accepts T or its subtypes (read-only operations):
import java.util.List;
class Vehicle {}
class Car extends Vehicle {}
class Bike extends Vehicle {}
class Truck extends Car {} // Subtype of Car
public class WildcardExample {
// Method to print all vehicles (including subtypes)
public static void printVehicles(List vehicles) {
for (Vehicle v : vehicles) {
System.out.println(v);
}
}
public static void main(String[] args) {
List cars = List.of(new Car(), new Truck());
List bikes = List.of(new Bike());
printVehicles(cars); // ✅ Works: Car is a subtype of Vehicle
printVehicles(bikes); // ✅ Works: Bike is a subtype of Vehicle
// vehicles.add(new Vehicle());
// ❌ Compile Error: Can't add (type unknown)
}
}
vehicles.add(new Vehicle()); ❌
Compile Error
2. Lower-Bounded Wildcard (? super T)
- Purpose: Accept a type
Tor any of its supertypes (write operations). - Use Case: When you need to add data to a generic structure.
Accepts T or its supertypes (write operations):
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// Add a Car or its subtype (e.g., Truck) to a list of Car's supertypes
public static void addCar(List list) {
list.add(new Car());
list.add(new Truck()); // ✅ Truck is a subtype of Car
}
public static void main(String[] args) {
List vehicles = new ArrayList<>();
List cars = new ArrayList<>();
addCar(vehicles); // ✅ Vehicle is a supertype of Car
addCar(cars); // ✅ Car is the same as Car
// ❌ Can't add to List:
// Vehicle v = vehicles.get(0);
// ❌ Compile Error: Type is unknown
}
}
3. Unbounded Wildcard (?)
- Purpose: Accept any type when you don’t care about the generic type.
- Use Case: When using methods that depend on
Objectbehavior (e.g.,toString()).
Works with any type (limited to Object methods):
import java.util.List;
public class WildcardExample {
// Print any list regardless of its type
public static void printList(List list) {
for (Object item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
List strings = List.of("A", "B");
List numbers = List.of(1, 2);
printList(strings); // ✅ Works
printList(numbers); // ✅ Works
// list.add("C"); ❌ Compile Error: Type is unknown
}
}
Wildcard Rules Table
| Wildcard Type | Syntax | Allowed Operations | Use Case |
|---|---|---|---|
| Upper-Bounded | ? extends T |
Read-only | Processing data |
| Lower-Bounded | ? super T |
Write | Adding data |
| Unbounded | ? |
Object methods | Generic utilities |
📚 PECS Principle Deep Dive
-
📦 Producer Extends: Use
? extends Twhen getting values -
🛒 Consumer Super: Use
? super Twhen adding values
What is PECS?
PECS stands for "Producer Extends, Consumer Super" - a mnemonic for deciding when to use
? extends T (upper bounds) vs ? super T (lower bounds) with wildcards.
Core Concept:
- ➡️ Producer: A data structure you read from (source)
- ⬅️ Consumer: A data structure you write to (destination)
1. Producer Extends (? extends T)
When to use: When you need to retrieve elements from a structure
// Valid operations
public double sumNumbers(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) { // Reading from producer
total += n.doubleValue();
}
return total;
}
// Usage:
List<Integer> ints = List.of(1, 2, 3);
sumNumbers(ints); // ✅ Works: Integer extends Number
// ❌ Invalid operation
numbers.add(10); // Compile error! Can't write to producer
Why this works:
The wildcard ? extends Number guarantees that every element is at least a Number,
but prevents adding elements because the actual type could be Integer, Double, etc.
2. Consumer Super (? super T)
When to use: When you need to insert elements into a structure
// Valid operations
public void fillWithIntegers(List<? super Integer> list) {
for (int i = 0; i < 10; i++) {
list.add(i); // Writing to consumer
}
}
// Usage:
List<Number> numbers = new ArrayList<>();
fillWithIntegers(numbers); // ✅ Works: Number super Integer
// ❌ Invalid operation
Number n = numbers.get(0); // Compile error! Can't read specific type
Why this works:
The wildcard ? super Integer allows adding Integer instances to any collection
that can hold them (e.g., Number or Object lists), but prevents reading specific types
because the actual type could be a supertype of Integer.
PECS Decision Table
| Scenario | Wildcard | Allowed Operations | Example Use Case |
|---|---|---|---|
| Reading elements | ? extends T |
✔️ Iteration ✔️ Access elements as T |
Processing elements from a collection |
| Writing elements | ? super T |
✔️ Add T instances ❌ Read specific types |
Populating a collection |
Real-World PECS Examples
1. Java Collections.copy()
public static <T> void copy(
List<? super T> dest, // Consumer (writing to)
List<? extends T> src // Producer (reading from)
) {
// Implementation
}
2. Stream API
// Producing elements Stream<? extends Number> stream = Stream.of(1, 2.5, 3L); // Consuming elements stream.forEach((Number n) -> System.out.println(n));
Common PECS Pitfalls
- Mixing Read/Write:
// Anti-pattern! public void process(List<? extends Number> list) { list.add(10); // Compile error - can't write to producer - Over-constraining:
// Unnecessarily restrictive public <T> void addAll(List<T> list, T... items) { ... } // PECS-improved version public <T> void addAll(List<? super T> list, T... items) { ... }
Why Wildcards Matter
- Flexibility: Write methods that work with a wide range of types.
- Type Safety: Prevent runtime errors by restricting operations (e.g., disallow unsafe adds/reads).
- API Design: Used extensively in Java’s Collections Framework (e.g.,
Collections.copy()).
Combined Example
// Copy all elements from a producer list (source) to a consumer list (dest) public staticvoid copy( List source, // Producer: Read from source List dest // Consumer: Write to dest ) { for (T item : source) { dest.add(item); } } // Usage: List cars = List.of(new Car(), new Truck()); List vehicles = new ArrayList<>(); copy(cars, vehicles); // ✅ Cars copied to vehicles
Key Takeaways
- Upper-Bounded (
? extends T): Read-only access toTand its subtypes. - Lower-Bounded (
? super T): Write access toTand its supertypes. - Unbounded (
?): Work with any type (rarely used directly). - PECS ensures maximum flexibility while maintaining type safety
- Wildcards create use-site variance in Java's generics system
Wildcards ensure your code is both flexible and type-safe! 🚀
