Generics let you write a class or method that works with any type while still catching type errors at compile time - you have already been using them every time you write List<String>.
A generic class
public class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // no casting needed
Box<Integer> intBox = new Box<>();
intBox.set(42);
T is a placeholder for "whatever type you specify when you use this class" - the compiler enforces that a Box<String> can never accidentally hold an Integer.
Life before generics
Older Java code used Object and manual casting, which pushed type errors to runtime instead of compile time:
Object obj = "Hello";
String s = (String) obj; // works, but crashes at runtime if obj wasn't actually a String
Generic methods
public static <T> T firstElement(List<T> list) {
return list.get(0);
}
String first = firstElement(names); // T is inferred as String here
Bounded type parameters
You can restrict a generic type to only accept types that extend a particular class or interface:
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
This accepts a List<Integer>, List<Double>, or any other numeric list, while still rejecting a List<String> at compile time.