Generics
Type-safe containers and methods in Java: type parameters, generic classes and methods, bounded types, wildcards, and what type erasure means in practice.
Why generics exist
A list stores items, a cache keeps values, a method finds the
largest element: such code does not care what type of data it
handles. Without generics there are two bad options: write the code
once per type, or write it once for Object, the
superclass of every class. The second throws away the
static typing
that lets the compiler find mistakes. Before Java 5 (2004) the
collections worked exactly like that, and a
List<Object> still does:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Object> readings = new ArrayList<>();
readings.add(412.5);
readings.add(415.0);
readings.add("419.2"); // compiles: a List<Object> accepts anything
double total = 0;
for (Object item : readings) {
total += (Double) item; // a cast is needed to get a number back
System.out.println("Running total: " + total);
}
}
}
Running total: 412.5 Running total: 827.5 Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Double (java.lang.String and java.lang.Double are in module java.base of loader 'bootstrap')
The mistake shows up only when the program runs, and only when the
bad element is reached — perhaps months later, in production.
Generics move the check to
the compiler: a class, interface, record or method declares a
placeholder for a type, and the code that uses it says which type it
wants. With List<Double>, the same mistake does
not compile:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Double> readings = new ArrayList<>();
readings.add(412.5);
readings.add("419.2");
}
}
Main.java:8: error: no suitable method found for add(String)
The full message continues with the reason: "argument mismatch;
String cannot be converted to Double". (Plain
javac shortens this to "incompatible types: String
cannot be converted to Double".) As a bonus, getting an element out
of a List<Double> needs no cast.
Generic classes, records and interfaces
A generic class declares its type parameters in angle brackets after
its name. Inside the class, T is used like any other
type. Code that uses the class supplies a type argument: in
Batch<String>, String replaces
T everywhere.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Batch<String> tickets = new Batch<>("NOC");
tickets.add("INC-10231");
tickets.add("INC-10232");
Batch<Integer> readings = new Batch<>("E-001");
readings.add(412);
readings.add(415);
String first = tickets.first(); // no cast needed
int total = readings.first() + readings.last();
System.out.println(tickets + " first=" + first);
System.out.println(readings + " sum of first and last=" + total);
}
}
class Batch<T> {
private final String label;
private final List<T> items = new ArrayList<>();
Batch(String label) {
this.label = label;
}
void add(T item) {
items.add(item);
}
T first() {
return items.getFirst();
}
T last() {
return items.getLast();
}
@Override
public String toString() {
return label + " " + items;
}
}
NOC [INC-10231, INC-10232] first=INC-10231 E-001 [412, 415] sum of first and last=827
By convention type parameters are single capital letters:
T for "type", E for "element" (the JDK
declares interface List<E>), K and
V for the keys and values of a map (Map<K, V>). Records and interfaces can be generic too, and type arguments
can themselves be generic:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Pair<String, Integer> reading = new Pair<>("E-001", 412);
System.out.println(reading);
System.out.println(reading.first().length() + reading.second());
Map<String, List<Integer>> perMeter = new TreeMap<>();
perMeter.put("E-002", List.of(388, 391));
perMeter.put("E-001", List.of(412, 415));
System.out.println(perMeter);
}
}
record Pair<A, B>(A first, B second) { }
Pair[first=E-001, second=412]
417
{E-001=[412, 415], E-002=[388, 391]}
reading.first() is known to be a String,
so length() compiles (6 + 412 = 417). Records are
explained in
Records, Enums and Sealed Types; the generic interfaces you meet most are
Comparable<T>,
Comparator<T> and the collection interfaces of
Arrays and Collections.
The diamond operator
Writing the type arguments twice —
Map<String, List<Integer>> m = new TreeMap<String,
List<Integer>>()
— is tedious. The
diamond operator
<> (Java 7) lets the compiler infer them from the
context: new TreeMap<>(). Be careful when you
combine it with var, because then there is no context
to infer from:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
var anything = new ArrayList<>();
anything.add(412);
anything.add("INC-10231");
System.out.println(anything);
var readings = new ArrayList<Integer>();
readings.add(412);
System.out.println(readings);
}
}
[412, INC-10231] [412]
var anything = new ArrayList<>() becomes an
ArrayList<Object> and accepts anything — the very
problem generics were meant to solve. With var, write
the type argument on the right; otherwise, declare the type on the
left and use the diamond on the right.
Generic methods
A method can have its own type parameters, written before the return
type: static <T> T lastOf(List<T> items).
Each call may use a different type, and usually you do not write it:
the compiler infers T from the arguments and from the
variable the result is assigned to. When inference needs help, put
the type argument before the method name, as in
Main.<Integer>repeat(0, 2).
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> tickets = List.of("INC-10231", "INC-10232", "INC-10233");
List<Double> temperatures = List.of(17.5, 18.25, 16.0);
String lastTicket = lastOf(tickets);
double lastTemperature = lastOf(temperatures);
System.out.println(lastTicket + " " + lastTemperature);
List<String> placeholders = repeat("n/a", 3);
System.out.println(placeholders);
System.out.println(Main.<Integer>repeat(0, 2));
}
static <T> T lastOf(List<T> items) {
return items.get(items.size() - 1);
}
static <T> List<T> repeat(T value, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(value);
}
return result;
}
}
INC-10233 16.0 [n/a, n/a, n/a] [0, 0]
Many JDK methods are generic: List.of,
Collections.max, Optional.of,
Comparator.comparing. That is why
List.of("a", "b") returns a
List<String> without you saying so.
Bounded type parameters
Inside lastOf, T could be anything, so the
method can only use what every object has (toString,
equals). To find the largest element, the method needs
compareTo. A
bounded type parameter
restricts which types are allowed — and in return lets the method
use the bound's methods.
<T extends Comparable<T>> reads "any type
T that can be compared with another T".
For a bound, extends is used for interfaces as well as
classes.
import java.util.List;
public class Main {
public static void main(String[] args) {
System.out.println(max(List.of(412, 388, 415)));
System.out.println(max(List.of("Mei", "Amara", "Sanne")));
System.out.println(max(List.of(17.5, 18.25, 16.0)));
}
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.getFirst();
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
}
415 Sanne 18.25
Integer, String and
Double all implement Comparable. A record
that does not is refused at the call:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Meter> meters = List.of(new Meter("E-001"), new Meter("E-002"));
System.out.println(max(meters));
}
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.getFirst();
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
}
record Meter(String id) { }
Main.java:6: error: method max in class Main cannot be applied to given types;
The rest of the message says why: "inference variable T has incompatible bounds … upper bounds: Comparable<T>". Generic error messages are long; look for the line with "reason:".
A generic class can be bounded the same way. Because every
T below is a Number, the class may call
doubleValue():
import java.util.List;
public class Main {
public static void main(String[] args) {
Statistics<Integer> kwh = new Statistics<>(List.of(412, 415, 419));
Statistics<Double> celsius = new Statistics<>(List.of(17.5, 18.25, 16.0));
System.out.println(kwh.average());
System.out.println(celsius.average());
}
}
class Statistics<T extends Number> {
private final List<T> values;
Statistics(List<T> values) {
this.values = values;
}
double average() {
double sum = 0;
for (T value : values) {
sum += value.doubleValue(); // allowed: every T is a Number
}
return sum / values.size();
}
}
415.3333333333333 17.25
Several bounds are joined with &, a class bound (if
any) first:
<T extends Number & Comparable<T>>.
Wildcards
Integer is a subclass of Number, so it is
tempting to think a List<Integer> is a
List<Number>. It is not, and the compiler says
so:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> readings = new ArrayList<>(List.of(412, 415));
List<Number> numbers = readings;
}
}
Main.java:7: error: incompatible types: List<Integer> cannot be converted to List<Number>
If it were allowed, numbers.add(17.5) would put a
Double into a list of integers. Generic types are
therefore invariant: List<A> and
List<B> are unrelated types, whatever the
relation between A and B. (Arrays are not:
a String[] is an Object[], and the same
mistake is caught only at run time with an
ArrayStoreException.)
A
wildcard type written with
? solves this for parameters that should accept a
family of generic types.
List<? extends Number> means "a list of some type
that is Number or a subclass of it":
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> kwh = List.of(412, 415, 419);
List<Double> celsius = List.of(17.5, 18.25, 16.0);
System.out.println(total(kwh));
System.out.println(total(celsius));
}
static double total(List<? extends Number> values) {
double sum = 0;
for (Number value : values) {
sum += value.doubleValue();
}
return sum;
}
}
1246.0 51.75
The price: total may read elements as
Number, but it may not add anything to
values (except null), because it does not
know which subtype the list really holds. For the opposite situation
— a method that puts integers into a list — use a lower bound,
? super Integer: "a list of Integer or of
a superclass of it".
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> readings = new ArrayList<>();
List<Number> mixed = new ArrayList<>(List.of(17.5));
List<Object> anything = new ArrayList<>(List.of("E-001"));
addMissingDays(readings, 2);
addMissingDays(mixed, 2);
addMissingDays(anything, 2);
System.out.println(readings + " " + mixed + " " + anything);
}
static void addMissingDays(List<? super Integer> target, int days) {
for (int i = 0; i < days; i++) {
target.add(0);
}
}
}
[0, 0] [17.5, 0, 0] [E-001, 0, 0]
| Form | Accepts | Inside the method |
|---|---|---|
List<?> |
A list of any type | Read elements as Object; add nothing |
List<? extends Number> |
List<Number>,
List<Integer>,
List<Double>, …
|
Read as Number; add nothing |
List<? super Integer> |
List<Integer>,
List<Number>,
List<Object>
|
Add Integers; read only as Object
|
A common memory aid, from Joshua Bloch's book
Effective Java, is PECS: "producer
extends, consumer super". A parameter the
method reads from (it produces values) gets extends;
one it writes to (it consumes values) gets super. That
is why the JDK's Collections.max is declared with
Comparable<? super T>: it also works for types
such as LocalDate, which is comparable to its supertype
ChronoLocalDate rather than to
LocalDate itself.
Type erasure
Generics were added to Java 5 in a way that kept existing compiled
code and libraries working. The compiler checks all type arguments,
then removes them: in the class file,
List<String> and
List<Integer> are both plain List, a
type parameter becomes its bound (or Object), and the
compiler inserts the casts you no longer write. This is called
type erasure. At run time, the
JVM cannot tell the two lists apart:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> tickets = new ArrayList<>();
List<Integer> readings = new ArrayList<>();
System.out.println(tickets.getClass().getName());
System.out.println(readings.getClass().getName());
System.out.println(tickets.getClass() == readings.getClass());
}
}
java.util.ArrayList java.util.ArrayList true
What erasure rules out
Anything that would need the type argument at run time does not compile:
-
new T()("unexpected type … found: type parameter T") andnew T[10]("generic array creation") — the JVM would not know which class to create. -
obj instanceof List<String>whenobjis anObject("Object cannot be safely cast to List<String>");obj instanceof List<?>is allowed. - Two overloads that differ only in their type arguments:
import java.util.List;
public class Main {
static void print(List<String> tickets) { }
static void print(List<Integer> readings) { }
public static void main(String[] args) { }
}
Main.java:6: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
When generic code really needs the type at run time, the usual
solution is to pass it in as a Class<T> object,
often called a type token. String.class is a
Class<String>, so the compiler infers
T from it:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Object> cells = List.of("E-001", 412, new BigDecimal("0.32"), "INC-10231");
List<String> texts = onlyOfType(cells, String.class);
List<Integer> numbers = onlyOfType(cells, Integer.class);
System.out.println(texts + " " + numbers);
}
static <T> List<T> onlyOfType(List<Object> values, Class<T> type) {
List<T> result = new ArrayList<>();
for (Object value : values) {
if (type.isInstance(value)) {
result.add(type.cast(value));
}
}
return result;
}
}
[E-001, INC-10231] [412]
JSON libraries use the same trick:
readValue(json, MeterReading.class). For a type such as
List<MeterReading>, which has no class object of
its own, they offer a "type reference" class — see
Files, I/O and JSON.
Generics and primitive types
Because a type parameter is erased to Object (or its
bound), a type argument must be a reference type. The eight
primitive types are not allowed:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<int> readings = List.of(412, 415);
}
}
Main.java:5: error: unexpected type
Use the wrapper class instead: List<Integer>.
Java converts between int and Integer
automatically (autoboxing, explained in
Variables, Types and Operators), so readings.add(412) just works. The cost is up to
one object per number. For large amounts of numeric data, plain
arrays (int[]) and the primitive streams
(IntStream, see
Lambdas, Functional Interfaces and Streams) avoid it. Making generics work with primitives without that cost
is a long-term goal of the OpenJDK project Valhalla; its first part,
value objects (JEP 401), has been integrated into JDK 28 as a
preview feature; JDK 28 is still in development as of October 2026.
Raw types
A
raw type is a generic type used
without type arguments: List instead of
List<String>. It exists so that code written
before Java 5 still compiles, and it switches off the type checks
for that variable. Old tutorials and legacy code are full of it.
Assigning a typed list to a raw variable lets anything in:
import java.util.ArrayList;
import java.util.List;
public class Readings {
public static void main(String[] args) {
List<Integer> readings = new ArrayList<>();
List raw = readings; // raw type
raw.add("INC-10231"); // no error: the check is off
System.out.println(raw);
int first = readings.get(0); // line 10
}
}
Note: Readings.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
[INC-10231] Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap') at Readings.main(Readings.java:10)
The program fails on line 10, which contains no visible cast: the
cast was inserted by the compiler (erasure again), and the string
that broke it was added two lines earlier. A list that holds
elements of the wrong type like this is called
heap pollution. javac only prints a short
note; with -Xlint:all it names each problem:
Readings.java:7: warning: [rawtypes] found raw type: List
List raw = readings; // raw type
^
missing type arguments for generic class List<E>
where E is a type-variable:
E extends Object declared in interface List
Readings.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
raw.add("INC-10231"); // no error: the check is off
^
where E is a type-variable:
E extends Object declared in interface List
2 warnings