W3docs

Introduction to Java Generics

Why Java generics exist — type safety, code reuse, and eliminating casts in collections and APIs.

Generics are the feature that lets a class, interface, or method work over an unspecified type, then have the compiler pin that type down at the spot you use it. A List<String> is a list of strings, the compiler knows it, and any attempt to put a Date into it is rejected before the program ever runs. Before generics arrived in Java 5, the same list was a List of Object, and every read out of it needed a hand-written cast that might or might not succeed at runtime. Generics turned that runtime gamble into a compile-time check, and almost every modern Java API is shaped by them.

The problem generics solve

To see why generics exist, imagine a Java without them. A container that holds arbitrary things has to declare its contents as Object:

// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");

String first = (String) names.get(0);   // cast required, never checked by the compiler

Two problems. First, the cast is noise — every read out of the container needs one. Second, and worse, nothing stops someone from putting a Date into that same list:

names.add(new java.util.Date());        // compiler is fine with this
String oops = (String) names.get(2);    // ClassCastException at runtime

The bug shows up at the read, far from the write. The cast lies — it says "this is a String," and the JVM finds out only when it's too late to give you a useful stack frame near the real fault.

Generics fix both:

List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date());        // ❌ compile error — won't even build

String first = names.get(0);  // no cast — the compiler already knows it's a String

The angle-bracket <String> is the type parameter. It tells the compiler "this list holds Strings," and from that moment on every add and get is checked against that promise.

Three things you get for free

Generics buy you three concrete benefits, and they're the reason every collection, stream, and optional in the modern JDK is generic:

  • Stronger compile-time checks. The wrong-type insertion above is caught at build time, not in production. A class of ClassCastException simply stops happening.
  • No more casts. Reading from a Map<String, User> gives you a User, not an Object you have to cast. Less syntactic noise, less to read, less to maintain.
  • Code reuse without copy-paste. One List<E> class works for every element type. Before generics, the standard library either accepted Object everywhere or shipped a StringList, IntList, DateList, and so on. Now you write one class and let the caller parameterise it.

That last point is the bigger architectural win. Generics are how you write a container, an algorithm, or a callback shape once and have it apply to every type the caller could pass in.

A first generic class

The convention is to name a type parameter with a single uppercase letter — T for a generic "type", E for "element" of a collection, K/V for "key" and "value" of a map, R for "return". Here's the simplest possible generic class — a pair of two things of the same type:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) {
    this.first  = first;
    this.second = second;
  }

  public T first()  { return first; }
  public T second() { return second; }
}

The <T> after the class name introduces the type parameter. From there, T is usable inside the class anywhere a normal type could go. The caller picks T when they create the object:

Pair<String>  names   = new Pair<>("Ada", "Grace");
Pair<Integer> scores  = new Pair<>(100, 87);
String n1 = names.first();      // already a String, no cast
int s1    = scores.first();     // auto-unboxed from Integer

The empty <> on the right (the diamond operator, Java 7+) tells the compiler to infer the type from the left-hand declaration — you almost never have to repeat the type argument.

What gets parameterised, and what doesn't

A type parameter can stand in for:

  • The type of a field (private T value;)
  • A parameter or return type of a method (public T get() { ... }, void put(T value))
  • The element type of an array of that type (T[] items — with some caveats)

A type parameter cannot stand in for:

  • A primitive (Pair<int> is illegal — use Pair<Integer> and let autoboxing do the work)
  • A static field or static method's type parameter (the parameter belongs to an instance, not the class itself)
  • The target of new T() or instanceof T — Java erases generics at runtime, so the program has no T to construct or test against

The full list of "things you can't do" gets its own chapter at the end of this part — Java Generics Restrictions — once we've covered enough machinery to make the rules make sense.

A worked example: type safety vs. raw types, side by side

The program below builds the same container twice — once as a raw List (the pre-generics shape) and once as a List<String>. Both compile; only the parameterised one is safe.

java— editable, runs on the server

The raw version crashes mid-iteration because the loop trusted a cast it had no business trusting. The generic version made the same mistake unrepresentable — the bad add(42) won't compile in the first place. That shift from runtime to compile time is the whole reason generics exist.

What this part of the book covers

The remaining chapters in this part take generics apart one piece at a time:

  • Generic classes — the class-level type parameter you just saw, in more depth.
  • Generic methods — methods that introduce their own type parameter, independent of the class.
  • Generic interfaces — designing API contracts that parameterise over a type.
  • Bounded type parameters — saying "T must extend Number" so you can call methods on T.
  • Wildcards? extends T, ? super T, and the PECS rule that decides when to use each.
  • Type erasure — how the JVM implements generics under the hood, and why a few things you might expect to work don't.
  • Restrictions — the catalogue of things the language refuses to let you do, with the reasons behind each one.

Read them in order — every chapter assumes the previous ones.

What's next

Start with the most common shape — a class whose fields and methods are parameterised over a type the caller picks. Continue to Java Generic Classes.

Practice

Practice
A method declares `public static List getNames() { ... }` (no type parameter on the list). The caller writes `String first = getNames().get(0);`. Why does the compiler warn — and what's the danger if you ignore the warning?
A method declares `public static List getNames() { ... }` (no type parameter on the list). The caller writes `String first = getNames().get(0);`. Why does the compiler warn — and what's the danger if you ignore the warning?
Was this page helpful?