Generics

Generic Classes

15 min Lesson 2 of 13

Generic Classes

A generic class is a class that is parameterised over types. Instead of hardcoding String or Integer, you declare a type parameter — a placeholder that the caller fills in. The compiler then verifies every use, eliminating the cast errors that plagued raw collections before Java 5.

Why write a generic class?

Suppose you need a simple container that holds exactly one value and can return it. Without generics you have two bad options: write a separate class for each type (StringBox, IntBox, …) or use Object and cast at every call site. Both are painful. A generic class solves both problems at once.

Key idea: A generic class defines behaviour once; the compiler generates a type-safe version for every concrete use. You get reuse and safety at the same time.

Declaring a generic class

Place the type-parameter list — one or more names inside angle brackets — immediately after the class name:

public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } }

T is just a name; by convention single uppercase letters are used: T for type, E for element, K and V for key/value, R for return type. The compiler replaces T with whatever the caller provides.

Using a generic class

At the call site you provide the type argument in angle brackets when creating the object:

Box<String> nameBox = new Box<>("Alice"); // diamond operator infers String Box<Integer> ageBox = new Box<>(30); String name = nameBox.getValue(); // no cast needed int age = ageBox.getValue(); // auto-unboxed // The compiler rejects the wrong type immediately: // nameBox.setValue(99); // compile error: int is not a String
Diamond operator <>: Since Java 7 you can omit the type argument on the right-hand side of a declaration — the compiler infers it from the left. Always prefer new Box<>(...) over new Box<String>(...) to reduce noise.

Multiple type parameters

A class can have more than one type parameter. A classic example is a pair that holds two independent values:

public class Pair<A, B> { private final A first; private final B second; public Pair(A first, B second) { this.first = first; this.second = second; } public A getFirst() { return first; } public B getSecond() { return second; } @Override public String toString() { return "(" + first + ", " + second + ")"; } }
Pair<String, Integer> entry = new Pair<>("score", 95); System.out.println(entry.getFirst()); // score System.out.println(entry.getSecond()); // 95 Pair<String, String> coords = new Pair<>("lat", "lng");

Generic classes and their members

Every instance method inside a generic class can use the class-level type parameters. The type parameter acts as if it were a concrete type for the entire instance:

public class Stack<E> { private final java.util.ArrayDeque<E> deque = new java.util.ArrayDeque<>(); public void push(E item) { deque.push(item); } public E pop() { return deque.pop(); } public E peek() { return deque.peek(); } public boolean isEmpty() { return deque.isEmpty(); } }
Stack<String> words = new Stack<>(); words.push("hello"); words.push("world"); System.out.println(words.pop()); // world System.out.println(words.pop()); // hello

Static members cannot use the class type parameter

Common pitfall: Static fields and static methods belong to the class, not to an instance, so there is no bound type parameter for them. The compiler will reject private static T cache; inside a generic class. If you need a generic static utility, declare a generic method instead (covered in the next lesson).

Raw types — what to avoid

If you use a generic class without a type argument, Java lets you — for backward compatibility — but you lose all type safety:

Box rawBox = new Box("text"); // compiles with a warning String s = (String) rawBox.getValue(); // cast required — error-prone

Never use raw types in new code. They exist only to interoperate with pre-Java-5 libraries. Modern IDEs will flag them as warnings and static analysis tools will treat them as errors.

Putting it together — a generic Result type

Here is a real-world pattern: a Result<T> that encapsulates either a success value or an error message, without throwing exceptions:

public class Result<T> { private final T value; private final String error; private Result(T value, String error) { this.value = value; this.error = error; } public static <T> Result<T> success(T value) { return new Result<>(value, null); } public static <T> Result<T> failure(String error) { return new Result<>(null, error); } public boolean isSuccess() { return error == null; } public T getValue() { return value; } public String getError() { return error; } }
Result<Integer> ok = Result.success(42); Result<Integer> err = Result.failure("not found"); if (ok.isSuccess()) { System.out.println("Value: " + ok.getValue()); }
Notice: the static factory methods use their own <T> declaration (a generic method) because static context cannot access the class T. The important point here is how the class-level T flows through all the instance methods naturally.

Summary

  • Declare type parameters after the class name: class Foo<T>.
  • Use the diamond operator <> on the right-hand side to let the compiler infer the type.
  • Multiple parameters are allowed: class Pair<A, B>.
  • Instance members freely use the type parameter; static members cannot.
  • Avoid raw types — always provide a type argument.