Generic Classes & Methods
Generics, introduced in Java 5, allow classes, interfaces, and methods to operate on typed parameters. They provide compile-time type safety and eliminate the need for casts.
Type Parameters
By convention single uppercase letters are used:
T— Type (general purpose)E— Element (collections)K,V— Key, Value (maps)R— Return type (functions)A,B— additional type parameters on a pair/tuple
Generic Class Syntax
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}At use-site: Box<String> box = new Box<>(); (diamond operator <> infers the type argument).
Generic Methods
A method can declare its own type parameters independently of the class:
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}The type parameter <T> appears before the return type.
Bounded Type Parameters
Restrict what types can be substituted with extends:
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}Multiple bounds use &: <T extends Cloneable & Serializable>. Only one class bound is allowed and it must come first.
Generic Interfaces
public interface Transformer<I, O> {
O transform(I input);
}Raw Types — Avoid Them
A raw type is a generic class used without its type argument: List list = new ArrayList();. Raw types exist only for backwards compatibility with pre-Java-5 code. They bypass the type system, produce heap pollution, and generate compiler warnings. Always use parameterized types.
Code Examples
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; }
// Generic static factory — convenient alternative to constructor
public static <X, Y> Pair<X, Y> of(X x, Y y) {
return new Pair<>(x, y);
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
public static void main(String[] args) {
Pair<String, Integer> nameAge = Pair.of("Alice", 30);
System.out.println("Pair : " + nameAge);
System.out.println("First : " + nameAge.getFirst());
System.out.println("Second : " + nameAge.getSecond());
Pair<Double, Double> coords = new Pair<>(51.5074, -0.1278);
System.out.println("Coords : " + coords);
// Type safety — the following would be a compile error:
// String s = nameAge.getSecond(); // int, not String
}
}Pair<A,B> is type-safe at compile time. The diamond operator <> on the right side lets the compiler infer the type arguments, keeping the code concise.
import java.util.Arrays;
import java.util.List;
public class GenericMethods {
// <T extends Comparable<T>> means T must support compareTo
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Returns the first element, or null for an empty list
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// Swaps two elements in an array (generic method on arrays)
public static <T> void swap(T[] arr, int i, int j) {
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
// max works for any Comparable
System.out.println(max(3, 7)); // Integer
System.out.println(max("apple", "banana")); // String
System.out.println(max(3.14, 2.71)); // Double
// firstOrNull
List<String> names = Arrays.asList("Carol", "Dave", "Eve");
System.out.println(firstOrNull(names));
System.out.println(firstOrNull(List.of()));
// swap
String[] words = {"one", "two", "three"};
swap(words, 0, 2);
System.out.println(Arrays.toString(words));
}
}The bound <T extends Comparable<T>> lets us call compareTo() inside the method. Without a bound only methods from Object would be accessible on T.
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;
public class GenericStack<E> {
private final List<E> elements = new ArrayList<>();
public void push(E item) {
elements.add(item);
}
public E pop() {
if (isEmpty()) throw new EmptyStackException();
return elements.remove(elements.size() - 1);
}
public E peek() {
if (isEmpty()) throw new EmptyStackException();
return elements.get(elements.size() - 1);
}
public boolean isEmpty() { return elements.isEmpty(); }
public int size() { return elements.size(); }
@Override
public String toString() { return elements.toString(); }
public static void main(String[] args) {
// Stack of Strings
GenericStack<String> strStack = new GenericStack<>();
strStack.push("first");
strStack.push("second");
strStack.push("third");
System.out.println("Stack : " + strStack);
System.out.println("Peek : " + strStack.peek());
System.out.println("Pop : " + strStack.pop());
System.out.println("After : " + strStack);
// Stack of Integers — same class, different type argument
GenericStack<Integer> intStack = new GenericStack<>();
for (int i = 1; i <= 4; i++) intStack.push(i * 10);
System.out.println("IntStack: " + intStack);
System.out.println("Size : " + intStack.size());
}
}GenericStack<E> is a fully type-safe container. The same implementation works for any type without duplication or casting. Note how GenericStack<String> and GenericStack<Integer> are completely separate types at compile time.
Quick Quiz
1. What does the diamond operator `<>` do in `new ArrayList<>()`?
2. What is a raw type?
3. Where does a generic method's type parameter declaration appear?
4. Given `<T extends Comparable<T> & Serializable>`, how many class bounds can T have?
Was this lesson helpful?