Generics
Generics let you parameterize a class, interface, or method with a type, so the compiler can enforce type safety at compile time instead of you discovering a type mismatch at runtime. They were added in Java 5, and today they are everywhere — every collection in the Java Collections Framework is generic.
Life before generics
Before Java 5, collections like ArrayList stored plain Object references. That meant you could put anything into a list, and you had to manually cast every element back to its real type when you read it out. The compiler had no way to catch a mistake — it only showed up as a ClassCastException at runtime, possibly far away from the line that actually caused it.
Before generics — unsafe and verbose
List names = new ArrayList(); // raw type, stores Object
names.add("Alice");
names.add(42); // compiler allows this — no type checking!
String first = (String) names.get(0); // manual cast required
String second = (String) names.get(1); // compiles fine...
// ...but throws ClassCastException at runtimeThe type-safe version
With generics, you tell the compiler up front what type a collection (or class) is meant to hold, using angle brackets: List<String> means “a List of String objects.” The compiler then rejects the wrong type immediately, at compile time, and no cast is needed when reading elements back out.
After generics — safe and cast-free
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(42); // compile-time error: incompatible types
String first = names.get(0); // no cast needed — the compiler knows it's a StringThis is the core benefit of generics: catching type errors at compile time instead of runtime, and eliminating boilerplate casts for the caller.
Generic classes
You are not limited to using generics on built-in collections — you can write your own generic classes. A type parameter (conventionally a single uppercase letter like T, E, K, or V) acts as a placeholder for whatever type the caller supplies when they use the class.
A simple generic container class
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // no cast needed
Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("oops"); // compile-time error
}
}The same Box class works for String, Integer, or any other type — one class definition, many type-safe instantiations. This is the same idea ArrayList<T> uses internally.
Generics add compile-time type checking to classes, interfaces, and methods, eliminating manual casts and runtime
ClassCastExceptions.Angle-bracket syntax like
List<String>orBox<T>declares the type a generic is parameterized over.You can define your own generic classes with a type parameter such as
<T>on the class declaration.Type erasure removes generic type information at runtime, which is why
new T()andinstanceof Tare not allowed.