Generic Programming in Java
Generics let you write classes, interfaces, and methods that work with multiple types while keeping compile-time type safety. They are a core part of the Java Collections Framework and many modern Java APIs.
Why Use Generics?
Without generics, reusable containers often have to store values as Object. That forces callers to cast values back to the expected type and moves many mistakes from compile time to runtime.
A Simple Generic Class
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}The type parameter T is a placeholder. It becomes a concrete type when the class is used.
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
Box<Integer> intBox = new Box<>();
intBox.setItem(42);Benefits of Generics
- Type safety: incompatible assignments are detected by the compiler.
- Reusability: one implementation can serve many types.
- Less casting: returned values already have the expected type.
- Clearer APIs: method signatures communicate the types they consume and produce.
Generic Methods
A method can introduce its own type parameter even when the surrounding class is not generic.
public static <T> void printItem(T item) {
System.out.println("Item: " + item);
}printItem("Hello");
printItem(100);
printItem(3.14);Bounded Type Parameters
Sometimes a generic type must provide a certain capability. An upper bound expresses that requirement.
public static <T extends Number> double twice(T value) {
return value.doubleValue() * 2;
}Here, T must be Number or one of its subclasses.
Wildcards
A wildcard represents an unknown generic type.
public static void printFirst(List<?> items) {
System.out.println(items.get(0));
}List<?> means “a list of some type, but the exact type is unknown here.”
Upper-bounded wildcard
List<? extends Number>Useful when you mainly read values as Number.
Lower-bounded wildcard
List<? super Integer>Useful when you need to safely add Integer values.
? extends T when a structure produces values for you; use ? super T when you mainly put values into it.Generics with Collections
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);Generic Type Invariance
Even if Integer extends Number, List<Integer> is not a subtype of List<Number>.
List<Integer> ints = List.of(1, 2, 3);
// Not allowed:
// List<Number> nums = ints;Wildcards are used when flexible relationships between parameterized types are needed.
Type Erasure
Java implements most generics through type erasure. Generic type information is used heavily by the compiler, while many type parameters are erased from the runtime representation.
| Compile time | Runtime implication |
|---|---|
| Type arguments are checked. | Most type arguments are not directly available as ordinary runtime class information. |
| Unsafe assignments are rejected. | You cannot normally create new T() or new T[]. |
| Bridge methods may be generated. | Compatibility with polymorphism is preserved after erasure. |
Common Type Parameter Names
T— TypeE— ElementK— KeyV— ValueR— Result
Conclusion
Generics combine reusability with compile-time type safety. Start with generic classes and methods, then learn bounded parameters, wildcards, PECS, invariance, and type erasure to understand how Java's generic type system behaves in real APIs.