Skip to content
Laviyx Learn

Try

Java course menu

Java Generics: Generic Classes, Generic Methods, Bounds and Wildcards

Learn Java generics: why type parameters make code type-safe, generic classes like Box<T> and Pair<K, V>, generic methods, bounded types with extends, wildcards (? extends and ? super), the PECS rule, type erasure, and common restrictions.

Advanced11 min readUpdated
On this page

Every time you write List<String> or Map<String, Integer>, you are using generics. Generics let a class or method work with any type, while the compiler still checks that you use the types consistently. You get reusable code and type safety.

Why generics exist#

Before generics (Java 5), collections held plain Objects. You had to cast when reading, and mistakes only showed up at run time. With generics, the compiler catches them:

Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Asha");
        // names.add(42);           // compile error: int is not a String
        String first = names.get(0); // no cast needed
        System.out.println(first.toUpperCase());
    }
}
Output
ASHA

A generic class#

Declare one or more type parameters in angle brackets after the class name. By convention they are single capital letters: T (type), E (element), K and V (key and value):

Java
class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }

    void set(T value) {
        this.value = value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> text = new Box<>("hello");
        Box<Integer> number = new Box<>(42);

        System.out.println(text.get().length() + " " + (number.get() + 8));
        // text.set(5);   // compile error: Box<String> only holds Strings
    }
}
Output
5 50

Box<String> and Box<Integer> share one class definition, and each is checked for its own type.

Several type parameters#

Java
record Pair<K, V>(K key, V value) {
    Pair<V, K> swap() {
        return new Pair<>(value, key);
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> p = new Pair<>("age", 21);
        Pair<Integer, String> swapped = p.swap();
        System.out.println(p + " " + swapped);
    }
}
Output
Pair[key=age, value=21] Pair[key=21, value=age]

Records, classes and interfaces can all be generic.

Generic methods#

A method can declare its own type parameter, written before the return type. The compiler infers it from the arguments:

Java
import java.util.List;

public class Main {
    static <T> T firstOrDefault(List<T> list, T fallback) {
        return list.isEmpty() ? fallback : list.get(0);
    }

    static <T> void printAll(List<T> items) {
        for (T item : items) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        System.out.println(firstOrDefault(List.of("x", "y"), "none"));
        System.out.println(firstOrDefault(List.<Integer>of(), -1));
        printAll(List.of(1, 2, 3));
        printAll(List.of("a", "b"));
    }
}
Output
x
-1
1 2 3 
a b 

Bounded type parameters#

<T extends Something> restricts T to types that are (or extend/implement) Something, which lets the method use that type's methods:

Java
import java.util.List;

public class Main {
    static <T extends Comparable<T>> T largest(List<T> items) {
        T best = items.get(0);
        for (T item : items) {
            if (item.compareTo(best) > 0) {
                best = item;
            }
        }
        return best;
    }

    static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number n : numbers) {
            total += n.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(largest(List.of(3, 9, 4)));
        System.out.println(largest(List.of("pear", "apple", "plum")));
        System.out.println(sum(List.of(1, 2, 3)) + " " + sum(List.of(1.5, 2.5)));
    }
}
Output
9
plum
6.0 4.0

Without the bound, item.compareTo(best) would not compile, because an arbitrary T has no compareTo.

Wildcards: ? extends and ? super#

A List<Integer> is not a List<Number>, even though Integer is a Number. (If it were, you could add a Double to a list of integers through the List<Number> reference.) Wildcards make methods flexible about this:

  • List<? extends Number>: a list of some subtype of Number. You can read Numbers from it, but not add to it. It is a producer.
  • List<? super Integer>: a list of Integer or a supertype. You can add Integers to it. It is a consumer.
Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    static void addNumbers(List<? super Integer> target, int count) {
        for (int i = 1; i <= count; i++) {
            target.add(i);
        }
    }

    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        List<Object> objects = new ArrayList<>();
        addNumbers(numbers, 3);
        addNumbers(objects, 2);
        System.out.println(numbers + " " + objects);
    }
}
Output
[1, 2, 3] [1, 2]

The rule of thumb is PECS: Producer Extends, Consumer Super. Collections.copy(List<? super T> dest, List<? extends T> src) follows it exactly.

Type erasure and restrictions#

Generics are checked by the compiler and then erased: at run time, a List<String> and a List<Integer> are both just List. That design kept compatibility with older code, and it explains the main restrictions:

  • Type arguments must be reference types: List<int> is not allowed; use List<Integer>.
  • You cannot create new T() or new T[10].
  • You cannot check obj instanceof List<String> (only instanceof List<?>).
  • Static fields cannot use the class's type parameter.

Common mistakes#

Practice#

Practice exercise

Write a generic method <T> int countMatches(List<T> items, T target) that counts elements equal to target (using equals), and test it with a list of integers and a list of strings.

Show solution
Java
import java.util.List;

public class CountMatches {
    static <T> int countMatches(List<T> items, T target) {
        int count = 0;
        for (T item : items) {
            if (item.equals(target)) count++;
        }
        return count;
    }

    public static void main(String[] args) {
        System.out.println(countMatches(List.of(3, 7, 3, 3, 9), 3));
        System.out.println(countMatches(List.of("to", "be", "or", "not", "to", "be"), "be"));
    }
}
Output
3
2

Collections.frequency(items, target) does the same in the standard library.

Key takeaway#

Finished this lesson?