Java Generics: Wildcards Explained

Java Generics Wildcards Guide
Java Generics: Wildcards Explained

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)
    }
}
    
⚠️ Can't add elements: vehicles.add(new Vehicle()); ❌ Compile Error

2. Lower-Bounded Wildcard (? super T)

  • Purpose: Accept a type T or 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 Object behavior (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 T when getting values
  • 🛒 Consumer Super: Use ? super T when 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 static  void 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

  1. Upper-Bounded (? extends T): Read-only access to T and its subtypes.
  2. Lower-Bounded (? super T): Write access to T and its supertypes.
  3. Unbounded (?): Work with any type (rarely used directly).
  4. PECS ensures maximum flexibility while maintaining type safety
  5. Wildcards create use-site variance in Java's generics system

Wildcards ensure your code is both flexible and type-safe! 🚀

Previous Post Next Post
Buy Me A Coffee
Thank you for visiting. You can now buy me a coffee!