Generics
Generic classes and methods, type parameters, bounded types, wildcards and type erasure.
Imagine writing a Box class that can hold a book, then another for a phone, then another for a shoe. Generics let you write the class once, with a placeholder type, and the compiler then checks every use. They are why List<String> only accepts strings and why map.get(key) returns the right type without a cast.
Life before generics#
Before Java 5, collections held plain Objects:
With generics, the same mistake is caught by the compiler:
The <> on the right is the diamond operator: the compiler infers <String> from the left side.
Writing 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/V (key/value), R (result).
Each use picks a concrete type argument: Box<String>, Box<Integer>. Type arguments must be reference types, so use Box<Integer>, never Box<int>.
Multiple type parameters#
Records can be generic too, which makes small type-safe tuples like this very cheap to write.
Generic methods#
A method can declare its own type parameters, placed before the return type. They are often static utilities:
The compiler usually infers T from the arguments. You can specify it explicitly when needed: List.<String>of().
Bounded type parameters#
Sometimes the code needs to call methods on T. A bound restricts which types are allowed:
extends in a bound covers both classes and interfaces. Multiple bounds use &: <T extends Number & Comparable<T>>.
Generics and inheritance: the surprise#
Integer is a subtype of Number, but List<Integer> is NOT a subtype of List<Number>:
Generic types are invariant. To write methods that accept lists of related types, use wildcards.
Wildcards: ?, ? extends, ? super#
Remember PECS: Producer Extends, Consumer Super.
- If a parameter produces values you read, use
? extends T. - If it consumes values you write, use
? super T. - If both, use exactly
T.
The JDK follows this everywhere. For example, Collections.copy(List<? super T> dest, List<? extends T> src).
Type erasure#
Generics are a compile-time feature. After checking types, the compiler erases them: Box<String> and Box<Integer> both become plain Box in bytecode, with T replaced by its bound (Object by default) and casts inserted where needed. This kept Java backward-compatible, but it has consequences:
The usual workaround is to pass in what you need, for example a factory:
(StringBuilder::new is a constructor reference, explained in the lambdas lesson.)
A generic interface#
Interfaces can be generic too, and that is how most of the Collections framework and java.util.function are built:
This is essentially the shape of Spring Data's CrudRepository<T, ID>.
Common mistakes#
- Using raw types (
Listinstead ofList<String>), which gives up all type safety. The compiler warns: "unchecked call". - Expecting
List<Integer>to be assignable toList<Number>. - Trying
new T(),T.classor generic arrays. - Overusing wildcards in return types. Return concrete types like
List<String>; use wildcards in parameters. - Using primitives as type arguments (
List<int>).
What's next#
Generics power the most important library in Java: the Collections framework. Next we start with List, ArrayList, LinkedList and iterating safely.
Check your understanding
Quick quiz
1.What is the main benefit of
List<String>over a rawList?2.A method parameter is
List<? extends Number>. What can you do with it?3.Why does
new T()not compile inside a generic class?
Finished reading?
Mark this lesson complete to track your progress.