Generics
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide compile-time type safety and eliminate the need for casting.
Why Generics?
Before generics, collections stored Object references, requiring explicit casts and risking ClassCastException at runtime:
List list = new ArrayList();
list.add("hello");
Integer num = (Integer) list.get(0); // ClassCastException at runtime
With generics, the compiler catches type errors early:
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(42); // Compile error
String s = list.get(0); // No cast needed
Generic Classes and Interfaces
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
Common generic interfaces include List<T>, Map<K, V>, and Comparable<T>.
Generic Methods
A method can have its own type parameter, independent of the class:
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
The type parameter <T> appears before the return type.
Bounded Type Parameters
Restrict the types that can be used as type arguments:
// Upper bound: T must be Number or a subclass
public static <T extends Number> double sum(T[] numbers) {
double total = 0;
for (T n : numbers) {
total += n.doubleValue();
}
return total;
}
Multiple bounds are allowed: <T extends Number & Comparable<T>>.
Wildcards
Wildcards add flexibility when the exact type is unknown.
| Wildcard | Meaning |
|---|---|
? |
Unknown type |
? extends T |
Upper bounded — read-only producer |
? super T |
Lower bounded — write-only consumer |
// PECS: Producer Extends, Consumer Super
public static double sumList(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
public static void addIntegers(List<? super Integer> list) {
list.add(42);
}
Type Erasure
Generics are a compile-time feature. At runtime, type parameters are erased to their bounds (or Object):
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// Both have the same runtime type: List
System.out.println(strings.getClass() == integers.getClass()); // true
Because of erasure, you cannot:
- Create generic arrays:
new T[10]is illegal - Use
instanceofwith parameterized types:obj instanceof List<String>is illegal - Create instances of type parameters:
new T()is illegal
Common Patterns
Generic Singleton Pattern
public class GenericFactory {
private static final Object LOCK = new Object();
@SuppressWarnings("unchecked")
public static <T> T[] emptyArray() {
return (T[]) new Object[0];
}
}
Type Token Workaround
When you need runtime type information, pass a Class<T> token:
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
String s = createInstance(String.class);
Best Practices
- Prefer generic types over raw types
- Use bounded wildcards in public APIs (PECS principle)
- Avoid mixing raw and parameterized types
- Use
@SuppressWarnings("unchecked")sparingly and only when necessary