Menu

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.