Generics give compile-time type safety to every collection and most modern APIs.
Generics let a class or method work with a type that is chosen by the user. List<String> is a list that holds only strings: the compiler rejects add(42) and get returns a String with no cast. Before generics (Java 5), collections held Object and every read needed a cast that could fail at runtime.
The diamond <> lets the compiler infer type arguments: Map<String, List<Integer>> m = new HashMap<>();.
Type arguments must be reference types: write List<Integer>, not List<int>; autoboxing handles the conversion.
Writing your own generic class is simple — declare a type parameter after the name:
class Box<T> {
private T value;
void set(T v) { value = v; }
T get() { return value; }
}By convention type parameters are single capital letters: T (type), E (element), K/V (key/value), R (result).
A generic method declares its own type parameter before the return type: static <T> T firstOrNull(List<T> list). The compiler infers T from the arguments at each call site.
You can restrict a type parameter with a bound: <T extends Comparable<T>> means "any T that can compare itself to another T", which lets the method call compareTo.
Avoid raw types like plain List without a type argument. They exist only for compatibility with ancient code, disable the checks and produce "unchecked" warnings. Wildcards (? extends, ? super) and type erasure are covered in the advanced generics lesson.
Example
import java.util.List;
public class GenericsDemo {
record Pair<A, B>(A first, B second) { } // generic record
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T item : list) if (item.compareTo(best) > 0) best = item;
return best;
}
public static void main(String[] args) {
var p = new Pair<>("age", 42); // Pair<String, Integer>
int age = p.second(); // no cast needed
System.out.println(p + " " + age); // Pair[first=age, second=42] 42
System.out.println(firstOrDefault(List.<String>of(), "none")); // none
System.out.println(max(List.of(3, 9, 4))); // 9
System.out.println(max(List.of("pear", "apple"))); // pear
}
}Remember
- Type arguments give compile-time checks and remove casts
- Use the diamond
<>and let the compiler infer - Bounds like
<T extends Comparable<T>>let generic code call methods on T
Watch out for
- Using raw types (
List list = new ArrayList();) and losing all type checking.