Menu

Java Generics: Type-Safe Classes and Methods

What Java generics are, how to write generic classes and methods, bounded type parameters, wildcards, and why type erasure matters.

This page includes runnable editors - edit, run, and see output instantly.

Why Generics Exist

A generic type lets you write code once and reuse it for many types without giving up type safety. Instead of writing a separate IntBox, StringBox, and UserBox, you write one Box<T> where T is a placeholder the caller fills in.

You have already used generics every time you wrote ArrayList<String> or a HashMap like HashMap<String, Integer>. The <...> part is a type argument. This page shows how to write your own.

The alternative - storing everything as Object - throws away type information and forces ugly, error-prone casts:

That cast on the last line throws a ClassCastException at runtime - the kind of exception generics are designed to make impossible.

A Generic Class

Declare a type parameter in angle brackets after the class name. By convention it is a single uppercase letter: T for "type", E for "element", K/V for key/value.

Inside Box, every T becomes whatever the caller supplied. Box<String> is a box that only holds and returns String. The compiler rejects name.set(99) before the program ever runs.

The empty <> on the right (the diamond operator) lets the compiler infer the type argument from the left side, so you don't repeat <String> twice.

Generic Methods

A single method can have its own type parameter, independent of the class. Put the parameter <T> before the return type:

You never pass T explicitly - the compiler infers it from the argument. Generic methods are how utilities like Collections.sort or List.of stay type-safe across any element type.

Bounded Type Parameters

Sometimes a generic type only makes sense for some types. extends constrains the parameter so you can call methods of the bound. Here T extends Number means T is Number or any subclass (Integer, Double, ...), so doubleValue() is available:

Note that extends here means "is a subtype of", and it works for both classes and interfaces - <T extends Comparable<T>> is extremely common when you need to compare elements.

Wildcards: ? extends and ? super

A subtle gotcha: List<Integer> is not a List<Number>, even though Integer is a Number. Generics are invariant. Wildcards relax this when you only need to read or only need to write.

Use ? extends T for a producer you read from, and ? super T for a consumer you write to (the "PECS" rule - Producer Extends, Consumer Super):

A ? extends Number list lets you read elements as Number but not add to it (the compiler can't know the exact element type). A ? super Integer list lets you add Integers but reads come back as Object. Pick the wildcard that matches how the data flows.

Type Erasure and Its Limits

Generics are a compile-time feature. After compilation the type parameter is erased - at runtime Box<String> and Box<Integer> are both just Box. This keeps generics backward-compatible with old code, but it imposes real limits.

// None of these compile - the type parameter doesn't exist at runtime:
T value = new T();          // can't instantiate a type parameter
T[] array = new T[10];      // can't create a generic array
if (list instanceof List<String>) { } // can't test the type argument

Because the type is gone at runtime, you cannot ask "what was T?" through reflection, and you cannot overload methods that differ only by their generic argument (foo(List<String>) and foo(List<Integer>) erase to the same signature). When you genuinely need the type at runtime, pass a Class<T> token as a constructor or method parameter.

Next: Lambda Expressions

You saw that a generic method takes a type as a parameter. The next step is treating behavior as a parameter. Lambda expressions let you pass a chunk of code - a function - into a method, which is exactly how you'll sort, filter, and transform the generic collections you just learned to type safely.

Frequently Asked Questions

What are generics in Java?

Generics let you write a class or method that works with a type you specify later, instead of locking it to one concrete type. You declare a type parameter in angle brackets - class Box<T> - and the caller fills in the actual type - Box<String>. The compiler then enforces that type everywhere, so you catch mismatches at compile time and skip manual casts.

Why use generics instead of Object?

Using Object loses all type information: the compiler can't stop you from putting the wrong thing in, and you must cast every value coming out (risking a ClassCastException at runtime). Generics push that checking to compile time. List<String> simply won't accept an Integer, and get() already returns a String - no cast, no surprise at runtime.

What is type erasure in Java generics?

Type erasure means generic type information exists only at compile time. After compiling, List<String> and List<Integer> are both just List at runtime - the type parameter is erased. This is why you can't write new T[10], call list instanceof List<String>, or read a type parameter via reflection. Generics give you compile-time safety, not runtime type data.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED