12.2 Generics
What Are Generics?
Generics let you write classes and methods where the type is a parameter. Instead of hardcoding String or Integer, you write T and the caller specifies the actual type. This gives you type safety at compile time and eliminates the need for casts.
Introduced in Java 5, generics are now fundamental to collections, streams, and virtually all modern Java APIs.
1. Why Generics?
Before generics, Java collections stored Object:
// Pre-generics Java (avoid!)
List list = new ArrayList();
list.add("hello");
list.add(42); // mixing types — no compile-time protection
String s = (String) list.get(0); // need cast
String s2 = (String) list.get(1); // ClassCastException at RUNTIME!
With generics:
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(42); // COMPILE ERROR — caught early!
String s = list.get(0); // no cast needed
Benefits:
- Compile-time type safety— errors caught before runtime
- Eliminates casts— cleaner, less error-prone code
- Enables reusable algorithms— write once, works for any type
2. Generic Classes
// A simple generic container
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;
}
@Override
public String toString() {
return "Box[" + value + "]";
}
}
// Usage
Box<String> strBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
Box<Double> dblBox = new Box<>(3.14);
System.out.println(strBox.getValue()); // Hello
System.out.println(intBox.getValue()); // 42
// Multiple type parameters
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> pair = new Pair<>("Alice", 95);
System.out.println(pair.getFirst()); // Alice
System.out.println(pair.getSecond()); // 95
System.out.println(pair); // (Alice, 95)
3. Generic Methods
Methods can have their own type parameters, independent of the class:
public class GenericUtils {
// Generic method — T is inferred from the argument
public static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// Return a generic type
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException("List is empty");
return list.get(0);
}
// Multiple type parameters
public static <K, V> Map<V, K> invertMap(Map<K, V> original) {
Map<V, K> result = new HashMap<>();
original.forEach((k, v) -> result.put(v, k));
return result;
}
public static void main(String[] args) {
Integer[] arr = {1, 2, 3, 4, 5};
swap(arr, 0, 4);
System.out.println(Arrays.toString(arr)); // [5, 2, 3, 4, 1]
List<String> names = List.of("Alice", "Bob", "Charlie");
System.out.println(getFirst(names)); // Alice
Map<String, Integer> original = Map.of("one", 1, "two", 2);
Map<Integer, String> inverted = invertMap(original);
System.out.println(inverted); // {1=one, 2=two}
}
}
4. Bounded Type Parameters
Restrict which types can be used with a generic:
// Upper bound: T must be a Number or subclass
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T item : list) {
total += item.doubleValue(); // safe because T extends Number
}
return total;
}
// Multiple bounds: T must implement both Comparable and Serializable
public static <T extends Comparable<T> & java.io.Serializable> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Usage
List<Integer> ints = List.of(1, 2, 3, 4, 5);
List<Double> doubles = List.of(1.1, 2.2, 3.3);
System.out.println(sum(ints)); // 15.0
System.out.println(sum(doubles)); // 6.6
System.out.println(max(10, 20)); // 20
System.out.println(max("apple", "zebra")); // zebra
5. Wildcards
Wildcards (?) represent an unknown type — useful when you want to accept a range of parameterized types:
import java.util.*;
// Unbounded wildcard: accepts any List<?>
public static void printList(List<?> list) {
list.forEach(item -> System.out.print(item + " "));
System.out.println();
}
// Upper bounded wildcard: accepts List<Number> or any List<? extends Number>
// Use when you only READ from the collection (producer)
public static double sumList(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// Lower bounded wildcard: accepts List<Integer> or any List<? super Integer>
// Use when you only WRITE to the collection (consumer)
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) list.add(i);
}
public static void main(String[] args) {
printList(List.of(1, 2, 3)); // works
printList(List.of("a", "b", "c")); // works
printList(List.of(1.1, 2.2, 3.3)); // works
System.out.println(sumList(List.of(1, 2, 3))); // 6.0
System.out.println(sumList(List.of(1.1, 2.2, 3.3))); // 6.6
List<Number> nums = new ArrayList<>();
addNumbers(nums); // OK: Number is a supertype of Integer
System.out.println(nums); // [1, 2, 3, 4, 5]
}
PECS — Producer Extends, Consumer Super
A rule of thumb for wildcards:
- Producer(you read FROM it) → use
extends - Consumer(you write TO it) → use
super
// Copying elements: src is a producer, dest is a consumer
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T element : src) {
dest.add(element);
}
}
List<Integer> source = List.of(1, 2, 3);
List<Number> destination = new ArrayList<>();
copy(source, destination);
System.out.println(destination); // [1, 2, 3]
6. Type Erasure
Java generics use type erasure: the generic type information exists only at compile time. At runtime, List<String> and List<Integer> are both just List. The compiler inserts casts automatically.
// At compile time:
List<String> strings = new ArrayList<>();
strings.add("hello");
String s = strings.get(0);
// After type erasure (what the JVM actually runs):
List strings = new ArrayList();
strings.add("hello");
String s = (String) strings.get(0); // cast inserted by compiler
Consequences of type erasure:
// Cannot use instanceof with parameterized type
if (obj instanceof List<String>) { } // COMPILE ERROR
// Cannot create generic arrays directly
T[] arr = new T[10]; // COMPILE ERROR
// Workaround: use Object array and cast
Object[] arr = new Object[10];
T element = (T) arr[0]; // unchecked warning (suppressed with @SuppressWarnings)
// Cannot catch generic exceptions
// catch (SomeException<T> e) { } // COMPILE ERROR
// Static fields cannot use the class type parameter
class Box<T> {
// static T defaultValue; // COMPILE ERROR
}
7. Practical Example: Generic Stack
import java.util.*;
public class GenericStack<T> {
private final List<T> elements = new ArrayList<>();
public void push(T element) {
elements.add(element);
}
public T pop() {
if (isEmpty()) throw new EmptyStackException();
return elements.remove(elements.size() - 1);
}
public T 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();
}
// Generic utility: pop all and return as list
public List<T> popAll() {
List<T> result = new ArrayList<>(elements);
elements.clear();
return result;
}
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("Pop: " + strStack.pop());
System.out.println("Peek: " + strStack.peek());
// Stack of Integers for expression evaluation
GenericStack<Integer> intStack = new GenericStack<>();
int[] postfix = {3, 4, 5, '*', '+'}; // 3 + 4 * 5 in postfix = 3 4 5 * +
for (int token : postfix) {
if (token > 127) { // it's an operator
int b = intStack.pop();
int a = intStack.pop();
switch ((char) token) {
case '+' -> intStack.push(a + b);
case '*' -> intStack.push(a * b);
}
} else {
intStack.push(token);
}
}
System.out.println("3 + 4*5 = " + intStack.pop()); // 23
}
}
Generic type naming conventions:
T— Type (general purpose)E— Element (collections)K,V— Key, Value (maps)N— NumberR— Return type (functions)
Prefer bounded wildcards in public APIs:
// Too restrictive: only accepts exactly List<Number>
public double sum(List<Number> list) { ... }
// Better: accepts List<Integer>, List<Double>, List<BigDecimal>, etc.
public double sum(List<? extends Number> list) { ... }
Don't use raw types in new code:
// BAD: raw type loses all type safety
List list = new ArrayList();
Map map = new HashMap();
// GOOD: always specify type parameters
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();