Generic Classes
A generic class declares its own type parameters, letting one class definition work correctly and safely across many different types.
A generic class is a class declared with one or more type parameters in angle brackets after its name. The type parameter acts as a placeholder for a real type supplied when the class is instantiated, letting you write a single class body — like Box<T> — that works for Box<String>, Box<Integer>, or any reference type, with full compile-time type checking.
Declaring a Generic Class: Box<T>
The canonical example is a simple container class that holds one value of an unspecified type, decided by whoever instantiates it.
T appears in the class header, and then anywhere inside the class a type is needed — the field type, the constructor parameter type, and both method's parameter/return types.
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T get() {
return content;
}
public void set(T content) {
this.content = content;
}
}Type Parameters
The <T> after the class name declares a type parameter. It is not a real type by itself — it is resolved to an actual type (called the type argument) at the point where the class is used.
Box<String> stringBox = new Box<>("hello"); // T is bound to String here
Box<Integer> intBox = new Box<>(42); // T is bound to Integer here
String s = stringBox.get(); // no cast needed
Integer i = intBox.get(); // no cast neededInstantiation and the Diamond Operator
Since Java 7, you can omit the repeated type argument on the right-hand side of a declaration using the diamond operator <> — the compiler infers it from the declared variable type or constructor context.
Box<String> a = new Box<String>("explicit"); // pre-Java-7 style, still legal
Box<String> b = new Box<>("inferred"); // diamond operator: preferredMultiple Type Parameters
A generic class can declare more than one type parameter, separated by commas, letting it work across several independent types at once.
K and V are independent placeholders — K becomes String and V becomes Integer for this particular Pair instance.
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> entry = new Pair<>("age", 30);
System.out.println(entry.getKey() + " = " + entry.getValue()); // age = 30Generic Fields and Methods Within the Class
Inside a generic class, the class's type parameter(s) can be used anywhere a type is needed: field declarations, method parameters, method return types, and local variable declarations.
import java.util.ArrayList;
import java.util.List;
public class Stack<T> {
private final List<T> items = new ArrayList<>();
public void push(T item) {
items.add(item);
}
public T pop() {
if (items.isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return items.remove(items.size() - 1);
}
public boolean isEmpty() {
return items.isEmpty();
}
}
Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
System.out.println(stack.pop()); // secondType Parameters vs. Instance Fields (static Context)
A class's type parameter belongs to each instance, not to the class itself, because at runtime there is really only one class (see Type Erasure). Consequently, a static member cannot use the class's type parameter — there's no single T shared across all instances for a static field or method to refer to.
A static factory method solves this by introducing its own, independent type parameter (here, U) rather than reusing the class's T.
public class Box<T> {
private T content;
// static T defaultValue; // compile-time error: cannot reference T in a static context
static <U> Box<U> of(U value) { // a static method can declare its OWN type parameter
return new Box<>(value);
}
}Instantiation Requires a Reference Type
Because of type erasure, a type argument must be a reference type — you cannot instantiate Box<int>; use the wrapper class Box<Integer> and rely on autoboxing.
Box<Integer> intBox = new Box<>(5); // autoboxed int -> Integer
int value = intBox.get(); // auto-unboxed Integer -> intGeneric Classes Implementing Generic Interfaces
A generic class can implement a generic interface, either passing through its own type parameter or fixing a concrete type argument.
import java.util.Comparator;
public class ByLength implements Comparator<String> { // fixed type argument: String
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
}
public class Wrapper<T> implements Comparable<Wrapper<T>> { // reuses T
private final T value;
private final int priority;
Wrapper(T value, int priority) {
this.value = value;
this.priority = priority;
}
@Override
public int compareTo(Wrapper<T> other) {
return Integer.compare(this.priority, other.priority);
}
}Practical Usage
Generic classes are the backbone of the Collections Framework (ArrayList<E>, HashMap<K,V>), and are equally useful in application code for reusable containers: caches, results wrappers, event objects, and typed builders.
public class Result<T> {
private final boolean success;
private final T value;
private final String error;
private Result(boolean success, T value, String error) {
this.success = success; this.value = value; this.error = error;
}
public static <T> Result<T> ok(T value) {
return new Result<>(true, value, null);
}
public static <T> Result<T> failure(String error) {
return new Result<>(false, null, error);
}
public boolean isSuccess() { return success; }
public T getValue() { return value; }
public String getError() { return error; }
}Edge Cases
You cannot create an instance of the type parameter directly (new T()), and you cannot create a generic array directly (new T[10]), both because of type erasure — the runtime has no way to know what T actually is at those points.
public class Container<T> {
// T instance = new T(); // compile-time error
// T[] array = new T[10]; // compile-time error
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
return (T[]) new Object[size]; // legal workaround, but produces an unchecked warning
}
}Mental Model
Think of a generic class as a template with a blank to be filled in. The class body is written once, generically, using the blank (T) wherever a type would go; each time you write Box<SomeType>, you're filling in that blank, and the compiler checks every use of the class against the filled-in type as if the class had been written specifically for SomeType.
Quick Reference
class Box<T> {
T content;
Box(T content) { this.content = content; }
T get() { return content; }
}
Box<String> b = new Box<>("hi"); // diamond operator infers T = String
class Pair<K, V> { K key; V value; } // multiple type parameters
Restrictions:
- no static field/method may use the class's type parameter directly
- no "new T()" or "new T[]" directly
- type argument must be a reference type (use wrapper classes for primitives)Key Takeaways
A generic class declares type parameters in angle brackets after its name, e.g. class Box<T>, usable throughout the class body.
Type arguments are supplied at instantiation, e.g. Box<String>; the diamond operator <> lets the compiler infer the argument on the right side of a declaration.
A class can declare multiple, independent type parameters, e.g. Pair<K, V>.
A static member cannot use the enclosing class's type parameter; a static generic method must declare its own.
You cannot instantiate new T() or create new T[] directly, and type arguments must be reference types — both consequences of type erasure.
public class Box<T> { static T defaultValue; // does not compile } public class Box<T> { private T content; static <U> Box<U> empty() { // static method declares its own type parameter return new Box<>(null); } }
A class's type parameter is tied to each instance, not the class itself, so it cannot be used in a static context; a static method must declare its own independent type parameter if it needs one.
public class Box<T> { public T create() { return new T(); // does not compile } } public class Box<T> { public T create(java.util.function.Supplier<T> factory) { return factory.get(); // caller supplies how to create a T } }
Type erasure means T's actual class isn't available at runtime, so new T() is illegal; pass in a factory (like a Supplier<T> or a Class<T> token) instead.
Box box = new Box("hello"); // raw type — loses type safety Box<String> box = new Box<>("hello");
Omitting the type argument entirely creates a raw type, which disables compile-time type checking for that variable; always supply a type argument (or let the diamond operator infer it).
What does class Box<T> declare, and where can T be used inside the class?
It declares Box as a generic class with type parameter T. T can be used anywhere a type is needed inside the class body: field types, constructor and method parameter types, method return types, and local variable types.
What does the diamond operator <> do?
It lets the compiler infer the type argument for the constructor call from context (typically the declared variable type), so you don't have to repeat it, e.g. Box<String> b = new Box<>("x"); instead of new Box<String>("x").
Why can't a static field or method use a generic class's type parameter directly?
A type parameter belongs to each instance of the class, not the class as a whole (there is conceptually one T per instantiation site), while a static member is shared by the class itself, so there's no single T for it to bind to. A static generic method must declare its own independent type parameter instead.
Why is new T() illegal inside a generic class?
Because of type erasure, the runtime does not retain enough information about what concrete type T was bound to at that generic class instance, so the JVM has no class to instantiate; workarounds involve passing a Supplier<T> or a Class<T> token.
How do you declare a generic class with two independent type parameters, and give an example use.
class Pair<K, V> { K key; V value; }; usage example: Pair<String, Integer> entry = new Pair<>("age", 30); where K is bound to String and V to Integer.
Interview: Why can a generic class implement a generic interface with a fixed, concrete type argument instead of passing through its own type parameter?
The class simply chooses to bind the interface's type parameter to a specific type rather than to its own type parameter, e.g. class ByLength implements Comparator<String> — this is valid because the interface only requires some type argument, not necessarily the implementing class's own T.
Interview: What compile-time check does declaring Stack<T> with an internal List<T> items field buy you that a Stack backed by List<Object> would not?
Every push()/pop() through the generic Stack<T> is checked against T at compile time, so a caller of Stack<String> cannot accidentally push an Integer, and pop() returns a String with no cast needed — whereas a List<Object>-backed stack would accept anything and require a manual, potentially-failing cast on every pop().
Write a generic class Triple<A, B, C> holding three values, with a method toString() printing them as "(a, b, c)".
public class Triple<A, B, C> { private final A first; private final B second; private final C third; public Triple(A first, B second, C third) { this.first = first; this.second = second; this.third = third; } @Override public String toString() { return "(" + first + ", " + second + ", " + third + ")"; } }
Three independent type parameters A, B, C let Triple hold values of three (possibly different) types, with toString() using Object's implicit toString() on each field via string concatenation.
What does this print?
Prints 15. h.get() returns an Integer (10) with no cast needed because Holder is parameterized with Integer, and it's auto-unboxed for the addition with 5.
Write a generic class Cache<K, V> backed by a HashMap, with put(K key, V value) and get(K key) methods.
import java.util.HashMap; import java.util.Map; public class Cache<K, V> { private final Map<K, V> store = new HashMap<>(); public void put(K key, V value) { store.put(key, value); } public V get(K key) { return store.get(key); } } // Usage: // Cache<String, Integer> cache = new Cache<>(); // cache.put("count", 5); // Integer count = cache.get("count");
Cache<K, V> forwards its two type parameters straight through to an internal Map<K, V>, giving callers a fully type-checked key-value store without exposing the underlying HashMap implementation.
What does this print, and what is lost by declaring box this way?
Prints raw. It compiles and runs because Box box is a raw type, so it's still legal Java, but all compile-time type checking on value is lost — the field's declared type is effectively erased to Object for this usage, and any future code adding a wrong-typed value to box would not be caught by the compiler.
class Holder<T> {
private T value;
Holder(T value) { this.value = value; }
T get() { return value; }
}
Holder<Integer> h = new Holder<>(10);
Integer v = h.get();
System.out.println(v + 5);class Box<T> {
T value;
Box(T value) { this.value = value; }
}
Box box = new Box("raw"); // note: no type argument
Object o = box.value;
System.out.println(o);