JV

Java Fundamentals

19 lessons

Progress0%
1. Introduction to Java
1What is Java?
2. Variables and Data Types
1Primitive Types
3. Control Flow
ConditionalsLoops
4. Methods
Defining MethodsMethod Overloading
5. Object-Oriented Programming
Classes and ObjectsInheritanceInterfaces and Abstract Classes
6. Collections
ArrayList and LinkedListHashMap and HashSet
7. Exception Handling
Checked & Unchecked Exceptionstry-with-resources & Custom Exceptions
8. Generics
Generic Classes & MethodsWildcards & Type Erasure
9. Modern Java
Lambdas & Functional InterfacesStream API & Optional
10. File I/O
java.nio.file APIBuffered I/O & try-with-resources
All Tutorials
JavaGenerics
Lesson 15 of 19 min
Chapter 8 · Lesson 2

Wildcards & Type Erasure

Wildcards

Wildcards use ? to represent an unknown type, solving a core limitation of generics: List<Dog> is not a subtype of List<Animal> even though Dog extends Animal. Wildcards bridge this gap.

Unbounded wildcard List<?> Useful when you only need Object operations on elements, or when the method works regardless of the element type (e.g., printAll(List<?> list)). You can read from it (as Object) but cannot add elements (except null).

Upper bounded wildcard List<? extends Animal> The list holds some subtype of Animal. You can read elements as Animal, but cannot add (the compiler does not know the exact subtype). Use when you need to read / consume from a collection — "producer extends".

Lower bounded wildcard List<? super Dog> The list holds Dog or any ancestor of Dog. You can add Dog objects safely, but reading yields only Object. Use when you need to write / produce into a collection — "consumer super".

PECS Principle (Producer Extends, Consumer Super) Coined by Joshua Bloch in Effective Java:

  • If a structure produces values you read → use ? extends T
  • If a structure consumes values you write → use ? super T
  • If both → use the exact type T

Type Erasure

Generics are implemented via type erasure: type parameters are removed by the compiler and replaced with their bounds (or Object if unbounded). At runtime, List<String> and List<Integer> are both just List.

Consequences:

  • Cannot do new T() or new T[] (type is unknown at runtime).
  • Cannot use instanceof List<String> — only instanceof List<?> or instanceof List.
  • Casts to parameterized types generate "unchecked" warnings.
  • @SuppressWarnings("unchecked") silences the warning when you are certain the cast is safe, but always document why.

Reifiable types are types whose full type information is available at runtime: primitives, raw types, non-generic types, unbounded wildcards (List<?>), and arrays of reifiable types.

Code Examples

Upper bounded wildcard — reading from a producerjava
import java.util.Arrays;
import java.util.List;

public class UpperBoundedDemo {

    // ? extends Number: accepts List<Integer>, List<Double>, List<Number> etc.
    public static double sumList(List<? extends Number> list) {
        double sum = 0;
        for (Number n : list) {   // we can READ elements as Number
            sum += n.doubleValue();
        }
        return sum;
    }

    public static void printAll(List<?> list) {  // unbounded: read as Object
        for (Object obj : list) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> ints    = Arrays.asList(1, 2, 3, 4, 5);
        List<Double>  doubles = Arrays.asList(1.5, 2.5, 3.0);
        List<String>  strings = Arrays.asList("hello", "world");

        System.out.println("Sum ints   : " + sumList(ints));
        System.out.println("Sum doubles: " + sumList(doubles));
        // sumList(strings) would be a compile error — String is not a Number

        printAll(ints);
        printAll(strings);  // works because printAll takes <?>

        // Cannot add to ? extends — compiler rejects it:
        // ints.add(6);  // would be: List<Integer> — fine
        // List<? extends Number> nums = ints;
        // nums.add(6.0);  // compile error — type unknown
    }
}

? extends Number lets sumList accept any list of numbers without code duplication. The trade-off: you cannot safely add to such a list because the compiler cannot verify which exact subtype it holds.

Lower bounded wildcard — writing into a consumerjava
import java.util.ArrayList;
import java.util.List;

public class LowerBoundedDemo {

    // ? super Integer: accepts List<Integer>, List<Number>, List<Object>
    public static void addNumbers(List<? super Integer> list, int count) {
        for (int i = 1; i <= count; i++) {
            list.add(i);   // safe: Integer is-a whatever ? super Integer holds
        }
    }

    public static void main(String[] args) {
        List<Integer> intList    = new ArrayList<>();
        List<Number>  numberList = new ArrayList<>();
        List<Object>  objectList = new ArrayList<>();

        addNumbers(intList,    3);
        addNumbers(numberList, 3);
        addNumbers(objectList, 3);

        System.out.println("intList   : " + intList);
        System.out.println("numberList: " + numberList);
        System.out.println("objectList: " + objectList);

        // Reading from ? super yields only Object
        List<? super Integer> consumer = numberList;
        Object first = consumer.get(0);   // only Object, not Number or Integer
        System.out.println("First (Object): " + first);
        System.out.println("Class         : " + first.getClass().getSimpleName());
    }
}

? super Integer allows adding Integer values to any list in the Integer hierarchy. The trade-off: reading yields only Object because the compiler cannot know the exact supertype stored.

PECS pattern — copy utilityjava
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PECSDemo {

    /**
     * Copies all elements from src (producer) into dest (consumer).
     * src  uses ? extends T — we READ from it.
     * dest uses ? super T  — we WRITE into it.
     */
    public static <T> void copy(List<? extends T> src,
                                List<? super T>   dest) {
        for (T item : src) {
            dest.add(item);
        }
    }

    public static void main(String[] args) {
        List<Integer>  ints    = Arrays.asList(10, 20, 30);
        List<Number>   numbers = new ArrayList<>();
        List<Object>   objects = new ArrayList<>();

        // Integer is a Number, so we can copy ints -> numbers
        copy(ints, numbers);
        System.out.println("numbers: " + numbers);

        // Integer is an Object, so we can copy ints -> objects
        copy(ints, objects);
        System.out.println("objects: " + objects);

        // Cannot copy numbers -> ints: Number is not ? extends Integer
        // copy(numbers, ints);  // compile error

        // Type erasure demo
        List<String>  strList = new ArrayList<>(Arrays.asList("a", "b"));
        List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2));

        // At runtime both are just ArrayList — type erasure at work
        System.out.println("Same class? " +
            (strList.getClass() == intList.getClass()));

        // instanceof with parameterized types is not allowed:
        // if (strList instanceof List<String>) { }  // compile error
        // Use raw type or unbounded wildcard:
        System.out.println("Is List?    " + (strList instanceof List<?>));
    }
}

PECS in action: the copy method uses extends for the source (we read T values) and super for the destination (we write T values). Type erasure is demonstrated by the two lists sharing the same runtime class.

Quick Quiz

1. What does the PECS principle stand for?

2. Why can you NOT add elements (other than null) to a `List<? extends Number>`?

3. What happens to generic type information at runtime due to type erasure?

4. Which wildcard form should you use when you only need to call `Object` methods on list elements?

Was this lesson helpful?

PreviousNext