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.


Java OOPs Concepts Tutorial

BR

Bhuvanesh Raja





Get in Touch For Details