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:

// 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); } }