Generic classes and methods

Core Java · lesson 29 of 42 · 4 min read

Write your own generic type, bound it, and see what the compiler erases before runtime.

Open this lesson in the learning hub

Key points

  • A type parameter turns one class into many: Box<String> and Box<Integer> share one source file.
  • A generic method puts the parameter before the return type: static <T> T first(List<T> xs).
  • Bound it when you need behaviour: <T extends Comparable<T>> lets you call compareTo on T.
  • The compiler checks the types, then erases them: at runtime Box<Mug> is only a Box.
  • Erasure is why new T[] and x instanceof List<String> do not compile.
  • Never use a raw Box. It switches every check off, and the warning is the compiler begging you to stop.

Example

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class Main {

    // A generic class: the caller chooses T.
    static class Box<T> {
        private final T value;

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

        T get() { return value; }

        <R> Box<R> map(Function<T, R> f) { return new Box<>(f.apply(value)); }
    }

    // A bounded generic method: T must be comparable with itself.
    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;
    }

    public static void main(String[] args) {
        Box<String> name = new Box<>("java");
        System.out.println("Box<String>       -> " + name.get().toUpperCase());
        System.out.println("mapped to Integer -> " + name.map(String::length).get());

        System.out.println("largest int    : " + largest(List.of(3, 17, 8)));
        System.out.println("largest string : " + largest(List.of("pear", "apple", "fig")));

        List<String> words = new ArrayList<>();
        List<Integer> numbers = new ArrayList<>();
        words.add("checked at compile time");
        numbers.add(1);

        System.out.println("same class after erasure: " + (words.getClass() == numbers.getClass()));
        System.out.println("runtime class of both   : " + words.getClass().getName());
        System.out.println("the compiler kept the check; the runtime kept nothing");
    }
}

Generics are a compile-time promise; nothing of T survives into the bytecode.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Core Java course, and every lesson in it is listed on the Core Java contents page.