Generics: Bounded Types and Wildcards

Generics let you write classes, interfaces, and methods that operate on a type parameter determined at compile time. They eliminate casts, catch type errors early, and make APIs self-documenting. But generics go well beyond simple : bounded type parameters and wildcards are where real expressiveness lives.

Why Generics?

Before generics (Java 1.4 and earlier), collections stored Object. Every retrieval required a cast, and the cast could fail at runtime: the compiler had no way to catch the mistake.

Java
// Pre-generics code — dangerous
List names = new ArrayList();
names.add("Alice");
names.add(42);  // No compiler error — anything goes

String first = (String) names.get(0);  // OK at runtime
String second = (String) names.get(1); // ClassCastException at runtime!

Generics parameterise types, moving the type check to compile time:

Java
// With generics — safe at compile time
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(42);  // Compiler error — int is not a String

String first = names.get(0);  // No cast needed — type is known

Generics are implemented via type erasure: at runtime, the JVM sees raw types. The type parameter is a compile-time-only concept. This means you cannot do new T() or instanceof List<String> at runtime.

Type erasure also means that List<String> and List<Integer> are the same class at runtime: List. They are distinct types only during compilation.

Generic Classes

A generic class declares one or more type parameters in angle brackets after the class name. By convention, single capital letters are used: T for "Type", E for "Element", K/V for key/value, R for return type.

Java
// A generic Pair holding two values of potentially different types
public class Pair<A, B> {

    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first  = first;
        this.second = second;
    }

    public A getFirst()  { return first; }
    public B getSecond() { return second; }

    // Swap — returns a new Pair with types reversed
    public Pair<B, A> swap() {
        return new Pair<>(second, first);
    }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }

    public static void main(String[] args) {
        Pair<String, Integer> entry = new Pair<>("score", 100);
        System.out.println(entry);          // (score, 100)
        System.out.println(entry.swap());   // (100, score)

        Pair<Integer, Integer> coords = new Pair<>(10, 20);
        System.out.println(coords);         // (10, 20)
    }
}
Java
// Generic stack built on top of a dynamic array
public class Stack<E> {

    private Object[] elements;
    private int size = 0;

    @SuppressWarnings("unchecked")
    public Stack(int capacity) {
        elements = new Object[capacity];
    }

    public void push(E item) {
        ensureCapacity();
        elements[size++] = item;
    }

    @SuppressWarnings("unchecked")
    public E pop() {
        if (size == 0) throw new java.util.EmptyStackException();
        E item = (E) elements[--size];
        elements[size] = null;  // Eliminate stale reference
        return item;
    }

    @SuppressWarnings("unchecked")
    public E peek() {
        if (size == 0) throw new java.util.EmptyStackException();
        return (E) elements[size - 1];
    }

    public boolean isEmpty() { return size == 0; }
    public int size()        { return size; }

    private void ensureCapacity() {
        if (size == elements.length) {
            elements = java.util.Arrays.copyOf(elements, size * 2 + 1);
        }
    }
}

Generic Methods

Methods can introduce their own type parameters, independent of any type parameter on the enclosing class. The type parameter list appears before the return type.

Java
public class Collections {

    // Type inferred from arguments — no cast needed at call site
    public static <T> List<T> repeat(T item, int times) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < times; i++) list.add(item);
        return list;
    }

    // Swap two elements in an array — works for any array type
    public static <T> void swap(T[] array, int i, int j) {
        T temp     = array[i];
        array[i]   = array[j];
        array[j]   = temp;
    }

    // Return the first non-null value from a varargs list
    @SafeVarargs
    public static <T> T firstNonNull(T... values) {
        for (T v : values) {
            if (v != null) return v;
        }
        throw new NullPointerException("All values are null");
    }

    public static void main(String[] args) {
        List<String> hellos = repeat("hello", 3);
        System.out.println(hellos);   // [hello, hello, hello]

        String[] arr = {"a", "b", "c"};
        swap(arr, 0, 2);
        System.out.println(java.util.Arrays.toString(arr)); // [c, b, a]

        String result = firstNonNull(null, null, "found");
        System.out.println(result);   // found
    }
}

The compiler infers the type argument from context. You can supply it explicitly when inference fails: Collections.<String>repeat("x", 5).

Upper Bounded Wildcards (? extends T)

? extends T means "some unknown type that is T or a subtype of T". Use it when you want to read values from a generic structure but don't need to write to it: the producer extends half of the PECS principle.

Java
// Works with List<Integer>, List<Double>, List<Number>, List<Object>? — NO!
// Works with List<Integer>, List<Double>, List<BigDecimal> — anything extends Number
public static double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) {
        sum += n.doubleValue();  // Safe to call Number methods
    }
    return sum;
}

public static void main(String[] args) {
    List<Integer>    ints    = List.of(1, 2, 3);
    List<Double>     doubles = List.of(1.5, 2.5);
    List<Long>       longs   = List.of(10L, 20L, 30L);

    System.out.println(sumList(ints));    // 6.0
    System.out.println(sumList(doubles)); // 4.0
    System.out.println(sumList(longs));   // 60.0
}

The trade-off: you cannot call list.add() on a List<? extends Number> (except null), because the compiler does not know which specific subtype the list holds.

Java
// Copying elements from a source into a destination — upper bounded source
public static <T> void copy(List<? extends T> source, List<? super T> dest) {
    for (T item : source) {
        dest.add(item);
    }
}

public static void main(String[] args) {
    List<Integer> source = List.of(1, 2, 3);
    List<Number>  dest   = new ArrayList<>();
    copy(source, dest);
    System.out.println(dest);  // [1, 2, 3]
}

Lower Bounded Wildcards (? super T)

? super T means "some unknown type that is T or a supertype of T". Use it when you want to write values into a generic structure: the consumer super half of PECS.

Java
// Fill a list with n copies of value — list must be able to hold T or its supertypes
public static <T> void fill(List<? super T> list, T value, int count) {
    for (int i = 0; i < count; i++) {
        list.add(value);  // Safe — list accepts T or anything above T
    }
}

public static void main(String[] args) {
    List<Number> numbers = new ArrayList<>();
    fill(numbers, 42, 3);           // Integer is a Number — OK
    fill(numbers, 3.14, 2);         // Double is a Number — OK
    System.out.println(numbers);    // [42, 42, 42, 3.14, 3.14]

    List<Object> objects = new ArrayList<>();
    fill(objects, "hello", 2);      // String super-bounded by Object — OK
    System.out.println(objects);    // [hello, hello]
}

The PECS mnemonic summarises when to reach for each wildcard:

  • Producer (you read from it) → ? extends T
  • Consumer (you write into it) → ? super T
Java
// Real-world example: sort using a Comparator that can compare supertypes
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    // Comparator<? super T> allows a Comparator<Number> to sort a List<Integer>
    java.util.Collections.sort(list);
}

// Practical: addAll from a collection of a subtype
public <E> boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c) {
        if (add(e)) modified = true;
    }
    return modified;
}

Unbounded Wildcards (?)

? is the unbounded wildcard. It means "a list of some unknown type". Use it when the method body only uses methods from Object, or when you genuinely don't care about the element type.

Java
// Prints any list regardless of element type
public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

// Count nulls in any collection
public static int countNulls(Collection<?> collection) {
    int count = 0;
    for (Object o : collection) {
        if (o == null) count++;
    }
    return count;
}

public static void main(String[] args) {
    printList(List.of(1, 2, 3));          // 1 2 3
    printList(List.of("a", "b", "c"));    // a b c
    printList(List.of(true, false));      // true false

    List<String> withNulls = new ArrayList<>(Arrays.asList("x", null, "y", null));
    System.out.println(countNulls(withNulls));  // 2
}
You cannot add anything (except null) to a List<?>. The compiler treats the element type as completely unknown. Use an unbounded wildcard only when reads (through Object) are all you need.

A subtle but important distinction: List<Object> is not the same as List<?>. You can add any object to a List<Object>, but a List<String> is not a List<Object> due to invariance. A List<String> is a List<?>.

Multiple Bounds

A type parameter can be constrained by multiple bounds using &. The class bound (if any) must come first, followed by interface bounds.

Java
// T must extend Comparable AND implement Serializable
public static <T extends Comparable<T> & java.io.Serializable>
        T findMax(List<T> list) {
    if (list.isEmpty()) throw new java.util.NoSuchElementException();
    T max = list.get(0);
    for (T item : list) {
        if (item.compareTo(max) > 0) max = item;
    }
    return max;
}

// Clamp a value between min and max — T must be Comparable
public static <T extends Comparable<T>> T clamp(T value, T min, T max) {
    if (value.compareTo(min) < 0) return min;
    if (value.compareTo(max) > 0) return max;
    return value;
}

// Useful pattern: require both Printable and Closeable
interface Printable { void print(); }

public static <T extends Printable & AutoCloseable> void printAndClose(T resource)
        throws Exception {
    resource.print();
    resource.close();
}

public static void main(String[] args) {
    System.out.println(findMax(List.of("banana", "apple", "cherry")));  // cherry
    System.out.println(findMax(List.of(3, 1, 4, 1, 5, 9)));             // 9

    System.out.println(clamp(15, 0, 10));   // 10  (clamped to max)
    System.out.println(clamp(-3, 0, 10));   // 0   (clamped to min)
    System.out.println(clamp(7,  0, 10));   // 7   (within range)
}

Multiple bounds are also useful for generic algorithms that need to call methods from two different interfaces on the same object:

Java
// Process items that are both Comparable (for sorting) and Iterable (for iteration)
public static <T extends Comparable<T> & Iterable<T>> void process(T container) {
    // Can call container.compareTo(other) AND iterate with for-each
    for (T element : container) {
        System.out.println(element);
    }
}

Note: