- Why Generics?
- Generic Classes
- Generic Methods
- Upper Bounded Wildcards (<? extends T>)
- Lower Bounded Wildcards (<? super T>)
- Unbounded Wildcards (<?>)
- Multiple Bounds
Before generics (Java 1.4 and earlier), collections stored Object. Every retrieval required a cast,
and the cast could fail at runtime — the compiler had no way to catch the mistake.
// Pre-generics code — dangerous
List names = new ArrayList();
names.add("Alice");
names.add(42); // No compiler error — anything goes
String first = (String) names.get(0); // OK at runtime
String second = (String) names.get(1); // ClassCastException at runtime!
Generics parameterise types, moving the type check to compile time:
// With generics — safe at compile time
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(42); // Compiler error — int is not a String
String first = names.get(0); // No cast needed — type is known
Generics are implemented via type erasure: at runtime, the JVM sees raw types. The type
parameter is a compile-time-only concept. This means you cannot do new T() or
instanceof List<String> at runtime.
Type erasure also means that List<String> and List<Integer> are the same class
at runtime — List. They are distinct types only during compilation.
A generic class declares one or more type parameters in angle brackets after the class name. By convention,
single capital letters are used: T for "Type", E for "Element", K/V
for key/value, R for return type.
// A generic Pair holding two values of potentially different types
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
// Swap — returns a new Pair with types reversed
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
public static void main(String[] args) {
Pair<String, Integer> entry = new Pair<>("score", 100);
System.out.println(entry); // (score, 100)
System.out.println(entry.swap()); // (100, score)
Pair<Integer, Integer> coords = new Pair<>(10, 20);
System.out.println(coords); // (10, 20)
}
}
// Generic stack built on top of a dynamic array
public class Stack<E> {
private Object[] elements;
private int size = 0;
@SuppressWarnings("unchecked")
public Stack(int capacity) {
elements = new Object[capacity];
}
public void push(E item) {
ensureCapacity();
elements[size++] = item;
}
@SuppressWarnings("unchecked")
public E pop() {
if (size == 0) throw new java.util.EmptyStackException();
E item = (E) elements[--size];
elements[size] = null; // Eliminate stale reference
return item;
}
@SuppressWarnings("unchecked")
public E peek() {
if (size == 0) throw new java.util.EmptyStackException();
return (E) elements[size - 1];
}
public boolean isEmpty() { return size == 0; }
public int size() { return size; }
private void ensureCapacity() {
if (size == elements.length) {
elements = java.util.Arrays.copyOf(elements, size * 2 + 1);
}
}
}
Methods can introduce their own type parameters, independent of any type parameter on the enclosing class. The
type parameter list appears before the return type.
public class Collections {
// Type inferred from arguments — no cast needed at call site
public static <T> List<T> repeat(T item, int times) {
List<T> list = new ArrayList<>();
for (int i = 0; i < times; i++) list.add(item);
return list;
}
// Swap two elements in an array — works for any array type
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Return the first non-null value from a varargs list
@SafeVarargs
public static <T> T firstNonNull(T... values) {
for (T v : values) {
if (v != null) return v;
}
throw new NullPointerException("All values are null");
}
public static void main(String[] args) {
List<String> hellos = repeat("hello", 3);
System.out.println(hellos); // [hello, hello, hello]
String[] arr = {"a", "b", "c"};
swap(arr, 0, 2);
System.out.println(java.util.Arrays.toString(arr)); // [c, b, a]
String result = firstNonNull(null, null, "found");
System.out.println(result); // found
}
}
The compiler infers the type argument from context. You can supply it explicitly when inference fails:
Collections.<String>repeat("x", 5).
? extends T means "some unknown type that is T or a subtype of T". Use it when you want to
read values from a generic structure but don't need to write to it — the producer extends
half of the PECS principle.
// Works with List<Integer>, List<Double>, List<Number>, List<Object>? — NO!
// Works with List<Integer>, List<Double>, List<BigDecimal> — anything extends Number
public static double sumList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue(); // Safe to call Number methods
}
return sum;
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
List<Long> longs = List.of(10L, 20L, 30L);
System.out.println(sumList(ints)); // 6.0
System.out.println(sumList(doubles)); // 4.0
System.out.println(sumList(longs)); // 60.0
}
The trade-off: you cannot call list.add() on a List<? extends Number> (except
null), because the compiler does not know which specific subtype the list holds.
// Copying elements from a source into a destination — upper bounded source
public static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) {
dest.add(item);
}
}
public static void main(String[] args) {
List<Integer> source = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>();
copy(source, dest);
System.out.println(dest); // [1, 2, 3]
}
? super T means "some unknown type that is T or a supertype of T". Use it when you want to
write values into a generic structure — the consumer super half of PECS.
// Fill a list with n copies of value — list must be able to hold T or its supertypes
public static <T> void fill(List<? super T> list, T value, int count) {
for (int i = 0; i < count; i++) {
list.add(value); // Safe — list accepts T or anything above T
}
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
fill(numbers, 42, 3); // Integer is a Number — OK
fill(numbers, 3.14, 2); // Double is a Number — OK
System.out.println(numbers); // [42, 42, 42, 3.14, 3.14]
List<Object> objects = new ArrayList<>();
fill(objects, "hello", 2); // String super-bounded by Object — OK
System.out.println(objects); // [hello, hello]
}
The PECS mnemonic summarises when to reach for each wildcard:
- Producer (you read from it) → ? extends T
- Consumer (you write into it) → ? super T
// Real-world example: sort using a Comparator that can compare supertypes
public static <T extends Comparable<? super T>> void sort(List<T> list) {
// Comparator<? super T> allows a Comparator<Number> to sort a List<Integer>
java.util.Collections.sort(list);
}
// Practical: addAll from a collection of a subtype
public <E> boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c) {
if (add(e)) modified = true;
}
return modified;
}
? is the unbounded wildcard. It means "a list of some unknown type". Use it when the method body
only uses methods from Object, or when you genuinely don't care about the element type.
// Prints any list regardless of element type
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// Count nulls in any collection
public static int countNulls(Collection<?> collection) {
int count = 0;
for (Object o : collection) {
if (o == null) count++;
}
return count;
}
public static void main(String[] args) {
printList(List.of(1, 2, 3)); // 1 2 3
printList(List.of("a", "b", "c")); // a b c
printList(List.of(true, false)); // true false
List<String> withNulls = new ArrayList<>(Arrays.asList("x", null, "y", null));
System.out.println(countNulls(withNulls)); // 2
}
You cannot add anything (except null) to a List<?>. The compiler treats the element
type as completely unknown. Use an unbounded wildcard only when reads (through Object) are all you need.
A subtle but important distinction: List<Object> is not the same as List<?>.
You can add any object to a List<Object>, but a List<String> is
not a List<Object> due to invariance. A List<String>
is a List<?>.
A type parameter can be constrained by multiple bounds using &. The class bound (if any) must
come first, followed by interface bounds.
// T must extend Comparable AND implement Serializable
public static <T extends Comparable<T> & java.io.Serializable>
T findMax(List<T> list) {
if (list.isEmpty()) throw new java.util.NoSuchElementException();
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
// Clamp a value between min and max — T must be Comparable
public static <T extends Comparable<T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
// Useful pattern: require both Printable and Closeable
interface Printable { void print(); }
public static <T extends Printable & AutoCloseable> void printAndClose(T resource)
throws Exception {
resource.print();
resource.close();
}
public static void main(String[] args) {
System.out.println(findMax(List.of("banana", "apple", "cherry"))); // cherry
System.out.println(findMax(List.of(3, 1, 4, 1, 5, 9))); // 9
System.out.println(clamp(15, 0, 10)); // 10 (clamped to max)
System.out.println(clamp(-3, 0, 10)); // 0 (clamped to min)
System.out.println(clamp(7, 0, 10)); // 7 (within range)
}
Multiple bounds are also useful for generic algorithms that need to call methods from two different interfaces
on the same object:
// Process items that are both Comparable (for sorting) and Iterable (for iteration)
public static <T extends Comparable<T> & Iterable<T>> void process(T container) {
// Can call container.compareTo(other) AND iterate with for-each
for (T element : container) {
System.out.println(element);
}
}