Why generics exist
Before Java 5, a collection held plain Object. You could add anything to a List, but
every time you read a value back you had to cast it, and if you guessed wrong the
program blew up at runtime with a ClassCastException. Generics fix this by letting
you parameterize a class, interface, or method by type, so the same code works for
many types while the compiler still enforces correctness. The two payoffs are
compile-time type checking — a wrong type is a build error, not a 3 a.m. crash — and
no manual casts when you read values out.
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42); // compile error — caught at build time
String n = names.get(0); // no cast needed; the compiler knows it's a String
That single shift — moving type failures from runtime to compile time — is the whole reason generics matter, and it underpins everything else in this guide.
Generic classes
You declare a generic class by adding a type parameter in angle brackets after the class name. That parameter then stands in for a real type everywhere inside the class, and the caller supplies the concrete type when they create an instance.
class Box<T> { // T is the type parameter
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
Box<String> b = new Box<>();
b.set("hi");
String s = b.get(); // returns T = String, no cast
For that instance the type argument String replaces T everywhere, so the compiler
rejects b.set(42) and lets get() hand you a String directly. Generic interfaces
work identically — interface Repository<T> declares T, and an implementor either
fixes the type (class UserRepo implements Repository<User>) or stays generic
(class MemRepo<T> implements Repository<T>). Core JDK contracts like Comparable<T>,
Iterable<E>, and Comparator<T> are exactly this pattern, which is why
class Money implements Comparable<Money> gives you a type-safe compareTo(Money) with
no downcast.
Naming conventions for type parameters
Type parameters are single uppercase letters by convention, each hinting at a role. The compiler accepts any valid identifier, but following the convention makes a generic signature readable at a glance.
interface Map<K, V> { V get(K key); } // K = key, V = value
The common letters: T for a generic type, E for an element (used throughout
the collections framework), K and V for a map's key and value, N for a
number, R for a return type, and S/U when a second or third type is
needed. When you see <E> on a collection or <K, V> on a map, you already know what the
author meant — that shared vocabulary is the entire point.
Generic methods
A class isn't the only thing that can be generic. A generic method declares its own type parameter, written before the return type, and that parameter is scoped to the single method. It can live in any class, generic or not.
// <T> introduces the type parameter for this method only
static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
String s = firstOrNull(List.of("a", "b")); // T inferred as String
The <T> between the modifiers and the return type is what makes the method generic —
without it, T would be an undefined symbol. Crucially, a generic method's type parameter
is independent of any type parameter on the enclosing class, which is what lets a
plain utility class expose generic helpers without itself being generic.
Type inference and the diamond operator
You rarely have to write type arguments by hand because the compiler performs type
inference: it looks at the argument types and the target type (what the result
is assigned to) and deduces the type parameter for you. The most visible form is the
diamond operator <> (Java 7+), which lets you omit the type arguments on the right of
an assignment and infer them from the declared type on the left.
// verbose, pre-Java 7
Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();
// diamond — compiler infers <String, List<Integer>> from the left side
Map<String, List<Integer>> m2 = new HashMap<>();
This cuts boilerplate without giving up any safety — new HashMap<>() is still fully
generic, just inferred. When inference genuinely can't decide (typically when there are no
arguments to learn from), you can supply an explicit type witness before the method
name, as in Collections.<String>emptyList(). One caution with var: the right-hand side
must carry the type arguments, because there's no left-hand declaration to borrow from —
write new ArrayList<String>(), not new ArrayList<>(), or you'll infer
ArrayList<Object> by accident.
Raw types and why to avoid them
A raw type is a generic type used without its type argument — List instead of
List<String>. It exists only for backward compatibility with pre-Java-5 code, and
using one opts out of type safety: the compiler stops checking element types and
re-introduces exactly the runtime failures generics were built to prevent.
List raw = new ArrayList(); // raw type — unchecked
raw.add("hi");
raw.add(42); // no complaint — types not checked
String s = (String) raw.get(1); // ClassCastException at runtime
Raw types produce unchecked warnings, which are the compiler begging you to
parameterize. If you genuinely want a collection that holds anything, use List<Object>
instead — it keeps full checking on. Note that new HashMap() (no diamond) is a raw type,
while new HashMap<>() is not; the missing brackets are not a cosmetic detail.
Generic invariance
Here is the subtlety that trips up most people: even though String is a subtype of
Object, List<String> is not a subtype of List<Object>. Generics are
invariant. This feels wrong until you see what it prevents.
List<String> strings = new ArrayList<>();
List<Object> objs = strings; // compile error — and a good thing too
objs.add(42); // ...because this would slip an Integer in
String s = strings.get(0); // ...and this would explode at runtime
The compiler blocks the assignment so that the unsafe sequence can never happen. Arrays,
by contrast, are covariant — String[] is an Object[] — which is precisely why array
stores are checked at runtime and can throw ArrayStoreException. Generics chose
compile-time safety over array-style covariance, and wildcards (List<?>, a list of some
unknown type) exist to recover the flexibility invariance gives up.
Multiple type parameters
A generic type can take more than one parameter — just list them comma-separated. The
canonical example is Map<K, V>, parameterized by both a key type and a value type, but
you'll write your own Pair-style carriers all the time.
class Pair<A, B> {
final A first;
final B second;
Pair(A a, B b) { first = a; second = b; }
}
Pair<String, Integer> p = new Pair<>("age", 30);
String key = p.first; // A = String
int val = p.second; // B = Integer
Each parameter is independent, so Pair<String, Integer> and Pair<Integer, String>
are distinct, incompatible types. The compiler tracks every slot separately, which is what
makes a two-parameter container as type-safe as a one-parameter one.
Generic constructors and static methods
Two corners surprise people. First, a constructor can declare its own type parameter, independent of the class's, written before the constructor name. This is rare but useful when the constructor needs a type purely for its arguments.
class Holder<T> {
private T value;
// S is the constructor's own parameter, separate from class T
<S extends T> Holder(S initial) { this.value = initial; }
}
Second, a static method cannot see the class's type parameter, because that parameter
exists per instance and a static method belongs to the class. A static method that needs
generics must declare its own — which is exactly how factory methods like
List.of and the pattern below work.
class Box<T> {
T value;
// static <U> needed — the class's T is not in scope here
static <U> Box<U> of(U v) {
Box<U> b = new Box<>();
b.value = v;
return b;
}
}
Box<String> b = Box.of("hi"); // U inferred as String
Reusing the class's letter T for a static or constructor parameter is legal but
confusing — pick a different letter so readers know it's a separate, independently-scoped
type.
Recap
Generics parameterize code by type to deliver compile-time type checking and
cast-free reads — type errors become build errors instead of ClassCastExceptions.
Declare a generic class with <T> after the name, a generic method with <T>
before the return type (independent of the class), and lean on type inference and the
diamond operator so you rarely write type arguments by hand. Follow the
T/E/K/V naming conventions for readable signatures, and never fall back to
raw types — use List<Object> if you truly want anything. Remember that generics are
invariant (List<String> is not a List<Object>), that multiple type parameters are
independent, and that static methods and constructors declare their own type
parameters. Get these basics right and the rest of generics — wildcards, bounds, and
erasure — builds cleanly on top.