You’ve already used generics: List<String> is a list that can only hold Strings. Generics let a class or method work with any type, while the compiler still checks that you use it consistently. They’re everywhere in modern Java — collections, Optional<T>, Function<T, R>, streams — so reading them comfortably is essential before the advanced lessons.
Why they exist
Before generics (Java 5), a List held plain Objects:
List list = new ArrayList();
list.add("AX-200");
String sku = (String) list.get(0); // cast needed, and a wrong cast only fails at runtime
With generics the compiler knows what’s inside, so no cast is needed and mistakes are caught at compile time:
List<String> skus = new ArrayList<>();
skus.add(42); // compile error: not a String
A generic class
static class Box<T> {
private final T value;
Box(T value) { this.value = value; }
T get() { return value; }
}
Box<String> name = new Box<>("Acme");
Box<Integer> qty = new Box<>(24);
name.get().toUpperCase(); // the compiler knows it's a String
T is a type parameter — a placeholder filled in when you use the class. Common names: T (type), E (element), K/V (key/value), R (result).
Records can be generic too:
record Pair<A, B>(A first, B second) { }
Pair<String, Double> line = new Pair<>("AX-200", 8.50);
A generic method
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
The <T> before the return type declares the type parameter for this method only. Java infers T from the arguments.
Bounded types
Sometimes you need some capability from T. extends sets an upper bound:
static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T n : numbers) total += n.doubleValue(); // allowed: every Number has doubleValue()
return total;
}
Wildcards: ?
List<Integer> is not a List<Number>, even though Integer is a Number — otherwise you could add a Double to a list of Integers. Wildcards express “a list of some subtype”:
static void printAll(List<? extends Number> numbers) { ... } // read Numbers from it
static void addDefaults(List<? super Integer> target) { ... } // put Integers into it
A handy rule is PECS: Producer Extends, Consumer Super. If a collection produces values for you to read, use ? extends T; if it consumes values you write, use ? super T. You’ll see exactly this in the signatures of Collections.sort, Comparator and the stream methods.
The full example
// GenericsDemo.java — generic classes, generic methods and bounded types.
import java.util.ArrayList;
import java.util.List;
public class GenericsDemo {
// A generic class: T is decided by whoever uses it
static class Box<T> {
private final T value;
Box(T value) { this.value = value; }
T get() { return value; }
boolean isPresent() { return value != null; }
}
// A generic pair with two type parameters
record Pair<A, B>(A first, B second) { }
// A generic method: works for any type T
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
// Bounded type: only numbers allowed
static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T n : numbers) {
total += n.doubleValue();
}
return total;
}
// Wildcard: read from a list of any subtype of Number
static void printAll(List<? extends Number> numbers) {
numbers.forEach(n -> System.out.print(n + " "));
System.out.println();
}
public static void main(String[] args) {
Box<String> name = new Box<>("Acme");
Box<Integer> qty = new Box<>(24);
System.out.println(name.get().toUpperCase() + " " + (qty.get() + 1));
Pair<String, Double> line = new Pair<>("AX-200", 8.50);
System.out.println(line.first() + " costs " + line.second());
System.out.println(firstOrDefault(new ArrayList<String>(), "none")); // none
System.out.println(sum(List.of(1, 2, 3.5))); // 6.5
printAll(List.of(1, 2, 3));
}
}
Try it yourself
Write a generic method <T> List<T> repeat(T value, int times) that returns a list containing value times times.
Show a solution
static <T> List<T> repeat(T value, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(value);
}
return result;
}
// repeat("ok", 3) -> [ok, ok, ok]; repeat(7, 2) -> [7, 7]
Key takeaways
- Generics give you reusable code that is still type-checked.
<T>on a class or method declares a type parameter.<T extends X>requires a capability; wildcards make APIs flexible.- PECS: producers
extends, consumerssuper.