Java Generics for Dummies
Generics in Java act as a type-safety enforcement mechanism that allows you to specify the exact type of objects a class, interface, or method can operate on. They enable you to write reusable code without sacrificing type safety.
Type-Safety
Before generics (introduced in Java 5) it was not possible to specify the type of collections in Java. This led to many runtime exceptions, typically ClassCastException.
Without generics
List list = new ArrayList();
list.add("Hello");
list.add(123); // Compiler allows this
// Requires casting, and crashes at runtime if the type is wrong
String works = (String) list.get(0);
String breaks = (String) list.get(1); // Throws ClassCastException!
With generics
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // Compiler Error: incompatible types
Code-Reusability
Java generics improve code reusability by allowing you to write a single class, interface, or method that can operate on different data types while also providing compile-time type safety.
Here is a really simple example of how generics improve code reusability.
Without generics (Method Overloading)
// This printer does not spark joy :(
public class SadPrinter {
// You have to duplicate the exact same println logic for every type
public static void printItem(String item) {
System.out.println(item);
}
public static void printItem(Integer item) {
System.out.println(item);
}
public static void printItem(Double item) {
System.out.println(item);
}
// And so on...
}
Supporting each type safely means one near-identical method per type.
With generics
// This printer does spark joy :)
public class HappyPrinter {
public static <T> void printItem(T item) {
System.out.println(item);
}
}
One method with a type parameter <T> covers every type.
Wildcards
A wildcard in Java is represented by a question mark ? and stands for an unknown type in generics.
You can use them if you don’t know or don’t care about the actual type parameter:
public static void printList(List<?> list) {
list.forEach(System.out::println);
}
You could also do this of course:
public static <T> void printList(List<T> list) {
list.forEach(System.out::println);
}
The main difference is that the wildcard version (?) communicates the intent better:
“This method does not care about the type you pass it.”
Typically you would only use a generic type parameter (T) if the type appears more than once in the signature, for example to:
1. Return the type it received
public static <T> T returnType(T t) {
return t;
}
2. Enforce that two arguments have the same type
public static <T> void copyElements(List<T> source, List<T> dest) {...}
Most of the time though, wildcards are used to provide flexibility when passing generic collections (like List, Set, Map etc.) to methods, allowing the method to accept collections of different, yet related, types.
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class AnimalFeeder {
public static void feedOne(Animal animal) {}
// Option 1: Using a wildcard
public static void feedMany(List<? extends Animal> animals) {}
// Option 2: Using a generic type parameter
public static <T extends Animal> void feedAnimals(List<T> animals) {}
}
public class Test {
public static void main(String[] args) {
var cat = new Cat();
var dog = new Dog();
// This works
AnimalFeeder.feedOne(cat);
AnimalFeeder.feedOne(dog);
List<Cat> cats = List.of(cat);
List<Dog> dogs = List.of(dog);
// This also works
AnimalFeeder.feedMany(cats);
AnimalFeeder.feedMany(dogs);
AnimalFeeder.feedAnimals(cats);
AnimalFeeder.feedAnimals(dogs);
}
}
But why do you need ? extends Animal or T extends Animal? Why can’t you just use Animal directly?
public static void feedAnimalsStrict(List<Animal> animals) {}
Generics in Java are invariant. This means that even though Dog is a subtype of Animal, a List<Dog> is not a subtype of List<Animal>.
If you define the method as:
public static void feedAnimalsStrict(List<Animal> animals) {}
You would only be able to pass exactly a List<Animal>. If you tried to pass a List<Dog> or a List<Cat>, the compiler would throw an error.
By using the wildcard ? extends Animal, List<Dog> and List<Cat> become subtypes of List<? extends Animal>. This is called covariance. With T extends Animal, T is simply inferred as Dog or Cat.
When a list is typed with ? extends Animal, the compiler restricts you from adding new elements to it (except null), because it doesn’t know exactly what specific list type it is backing. It just knows it’s safe to read from the list and treat every item as an Animal.
PECS (Producer Extends Consumer Super)
PECS (Producer Extends, Consumer Super) is a rule for using wildcard types (?) in Java Generics to make your APIs as flexible and widely usable as possible.
It boils down to a really simple question: “What does your method do with the Collection you provide it?”
- Does it only read from the collection?
- Does it only write to the collection?
Its goal is to maximize the flexibility and usability of your code.
The rule can be easily displayed in the table below.
| Collection Role | Wildcard | Data Flow | Action Allowed |
|---|---|---|---|
| Producer | ? extends T | Data comes out of the collection | Read-only |
| Consumer | ? super T | Data goes into the collection | Write (read only as Object) |
You need this rule because Java generics are invariant, meaning that a List<Integer> is not recognized as a subtype of List<Number>.
Due to this, you need wildcards to let methods accept collections of subtypes or supertypes.
PECS tells you exactly which wildcard to use based on what your method does with the collection.
1. Producer extends (? extends T)
If your method takes a collection and you only need to read elements from it, that collection is a producer of data.
- What it allows: You can pass a collection of
Tor any of its subtypes. - The catch: To maintain type safety the compiler will not allow you to add anything (besides null) to the collection.
// This method only reads from the list.
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
// This works perfectly because of ? extends
List<Integer> ints = Arrays.asList(1, 2, 3);
sum(ints);
The method’s code only ever pulls data out of the list. In PECS this means that the list itself is a “Producer.”
And for a producer you should use ? extends T.
2. Consumer super (? super T)
If your method takes a collection and you only need to write/add elements to it, that collection is a consumer of data.
- What it allows: You can pass a collection of
Tor any of its supertypes (all the way up toObject). - The catch: You cannot read from this collection and guarantee its type (other than treating the elements as
Objects).
// This method only writes to the list.
// It "consumes" the Integers you give it.
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
// Usage:
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addNumbers(nums);
addNumbers(objs);