Writing Your Own Generic Types

OOP · lesson 33 of 43 · 4 min read

Type parameters on your own classes and methods, bounded types, and what erasure will not let you do.

Open this lesson in the learning hub

Key points

  • A type parameter is a hole the caller fills: Box<T> takes a T and hands a T back, with no cast anywhere.
  • A generic method declares its own parameter before the return type: static <T> T largest(List<T> items).
  • Bound it when the body needs behaviour: <T extends Comparable<T>> is what allows compareTo.
  • Generics are compile-time only. After erasure a Box<String> and a Box<Integer> are the same class.
  • So new T(), new T[10] and instanceof Box<String> are all illegal. Pass in a factory.
  • Generic types are invariant: a List<String> is not a List<Object>, which is why wildcards exist.

Example

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

public class Main {

    // One type parameter turns one class into a family of type-safe classes.
    static final class Box<T> {
        private final T value;

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

        static <T> Box<T> of(T value) { return new Box<>(value); }

        T get() { return value; }                       // no cast at the call site

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

        @Override public String toString() { return "Box[" + value + "]"; }
    }

    // Bounded: the body may call compareTo because T is known to have it.
    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 = Box.of("ada");
        String upper = name.get().toUpperCase();        // typed, so no cast
        System.out.println(name + " -> " + upper);
        System.out.println(name.map(String::length));

        System.out.println("largest int  = " + largest(List.of(3, 11, 7)));
        System.out.println("largest word = " + largest(List.of("pear", "apple", "fig")));

        // Erasure: the type argument is gone by the time the JVM runs this.
        System.out.println("same class?  = " + (Box.of(1).getClass() == Box.of("x").getClass()));
    }
}

Parameterise when only the type changes, and bound it when the body needs behaviour.

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 OOP course, and every lesson in it is listed on the OOP contents page.