Generic Classes and Methods
How to declare your own generic types and methods, and the small-but-deep set of design choices that come with them
We have used the standard generic types (List<E>, Map<K,V>,
Set<E>). It is time to write our own. Designing a generic class
is one of the most underrated skills in Java — it is what separates
a library you can use anywhere from one you have to copy-paste and
edit.
A first generic class: a typed box
The traditional "hello world" of generics is a Box<T> — a container
of exactly one element of some type T. Compare the untyped and
typed versions.
Three things to notice in the typed Box:
- The type parameter
<T>appears immediately after the class name. From inside the class,Tis just a placeholder type — wherever you would writeStringorCustomer, you can writeT. putandgetuseT. The compiler now enforces that whateverputaccepts is whatevergetreturns.- The diamond
<>onnew Box<>()lets the compiler inferStringfrom the variable's declared type.
Naming conventions
By convention, single uppercase letters name type parameters:
| Letter | Meaning |
|---|---|
E | An element of a collection |
T | A general type — the only or main type parameter |
K, V | A key and a value in a map |
R | A return type, especially in functional interfaces |
N | A numeric type |
S, U | Secondary type parameters when more than one is needed |
These are conventions, not rules — the compiler will accept any identifier — but every Java reader is wired to expect them, so straying is gratuitous noise.
Multiple type parameters
A generic class can take more than one type parameter. The classic
example is Pair<A, B>:
Each occurrence is checked independently — putting a String where
a B is expected only works if B = String for that particular
instance.
Generic methods (independent of the class)
A method can have its own type parameters, independent of the enclosing class. The type parameter declaration goes before the return type:
public static <T> T identity(T x) { return x; }Generic methods are how the standard library writes things like
Collections.emptyList() or Map.of(k, v). You usually do not have
to specify the type parameter at the call site — the compiler infers
it.
A multi-file generic class: a stack
This is the classic generic class to write next. Stack<E> is a
typed LIFO container — push, pop, peek, size.
Compare this to the JDK's legacy Stack: ours is generic from line
one, has no thread-safety penalty, and does not leak random Vector
methods into its public API. This is what programming to interfaces
- generics buys you.
A generic class with a constraint: numeric averages
Sometimes a type parameter must support some operation. For example,
a generic average method needs the elements to be Numbers. That
is what bounded type parameters are for, in their simplest form:
We will go far deeper into bounds and wildcards in the next chapter.
For now, the syntax <T extends Number> says "any T that is a
Number."
Constraints can also be on interfaces
extends works for interfaces too. A common case is requiring the
type to be Comparable:
You can also combine constraints with &:
<T extends Number & Comparable<T>> means "any T that is both a
Number and Comparable<T>."
Practice
Write a class Pair<A, B> with:
- A constructor that takes
A firstandB second. - Accessors
first()andsecond(). - A method
Pair<B, A> swap()that returns a new pair with the components reversed (note the type change!).
Expected output:
ada / 36
36 / ada
Test your understanding
Where does a generic method declare its own type parameters?
After the method name
In the class header
Immediately before the return type, e.g. public static <T> T identity(T x)
In the calling code only
What does the bound <T extends Number> allow the method body to do that <T> would not?
Call any method of Object on T
Call methods defined on Number (such as intValue(), doubleValue()) on any value of type T
Compare T values with < and >
Construct new instances of T
Why is class Stack<E> { private final List<E> data = new ArrayList<>(); … } a better design than the legacy java.util.Stack?
It uses less memory
It composes a List instead of extending Vector, so its public API contains only the stack operations — not random list methods
It is faster on every operation
The legacy Stack does not compile in modern Java
Equality and Hashing
The equals / hashCode contract — what every Set, Map, and contains() check secretly depends on, and the bugs that lurk when you get it wrong
Bounded Types and Wildcards
Why List<Dog> is not a List<Animal>, what ? extends T and ? super T really mean, and the PECS rule that makes it all click