Generics in Java OOP
Generics in Java OOP
Before generics arrived in Java 5, a general-purpose collection had to store everything as Object, forcing a manual, unchecked cast every time you read a value back out, with no compiler help if you got the type wrong. Generics let you write a single class or method that works across many types, while still catching type mismatches at compile time rather than at runtime.
Generic Classes and Methods
A generic class declares one or more type parameters, conventionally a single uppercase letter like T, in angle brackets right after the class name. Every place that type would normally appear inside the class is replaced with that placeholder.
class Box<T> {
private T content;
void set(T content) {
this.content = content;
}
T get() {
return content;
}
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // no cast needed — already known to be a String
Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("oops"); // COMPILE ERROR — caught immediately, not at runtime
A generic method introduces its own type parameter, independent of any class-level one, declared right before the return type. This lets a single static utility method work across any type its caller needs, without making the entire class generic.
class ArrayUtils {
static <T> void printAll(T[] items) {
for (T item : items) {
System.out.println(item);
}
}
}
ArrayUtils.printAll(new String[]{"a", "b", "c"});
ArrayUtils.printAll(new Integer[]{1, 2, 3});
Bounded Type Parameters
An unbounded type parameter like <T> accepts absolutely any type, including ones with no methods you can usefully call on them inside the generic class. A bounded type parameter restricts T to a specific type or any of its subtypes, written with extends, which guarantees the methods of that bound are available to use.
class NumberBox<T extends Number> { // T must be Number or a subclass of it
private T value;
NumberBox(T value) {
this.value = value;
}
double doubled() {
return value.doubleValue() * 2; // doubleValue() is guaranteed by the Number bound
}
}
NumberBox<Integer> intBox = new NumberBox<>(10);
System.out.println(intBox.doubled()); // 20.0
// NumberBox<String> strBox = new NumberBox<>("x"); // COMPILE ERROR — String isn't a Number
A type parameter can also be bounded by multiple types at once, using &, requiring it to satisfy every listed bound simultaneously. This is commonly used to require both a base class and an interface, such as being both a Number and Comparable.
static <T extends Number & Comparable<T>> T max(T a, T b) {
return (a.compareTo(b) > 0) ? a : b;
}
Wildcards in Generics
A wildcard, written ?, represents an unknown type and is used when you genuinely don't need to name a specific type parameter, typically in a method parameter, to make an API more flexible. Java supports three forms: unbounded, upper-bounded, and lower-bounded.
Wildcard Form | Syntax | Meaning |
Unbounded | List<?> | A list of some unknown type; you can read elements as Object, but can't safely add anything |
Upper-bounded | List<? extends Number> | A list of Number or any subtype of Number; safe to read, unsafe to add |
Lower-bounded | List<? super Integer> | A list of Integer or any supertype of Integer; safe to add Integer values, reads come back as Object |
// Upper-bounded wildcard — used to safely READ from a list of unknown Number subtype
static double sumOfList(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sumOfList(ints)); // 6.0
System.out.println(sumOfList(doubles)); // 4.0
// Lower-bounded wildcard — used to safely WRITE Integers into a list of unknown Integer supertype
static void addNumbers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
Joshua Bloch's well-known mnemonic from Effective Java captures the rule precisely: PECS, Producer extends, Consumer super. If a generic parameter only produces (you read from it), use extends. If it only consumes (you write to it), use super.
Did You Know?
Generics are a purely compile-time feature in Java, through a process called type erasure. After compilation, Box<String> and Box<Integer> both become simply Box at the bytecode level, with the compiler inserting the appropriate casts for you automatically wherever needed. This is also exactly why a generic type parameter cannot be a primitive type like int, since type erasure replaces every unbounded type parameter with Object, and primitives are not objects.










