Java Generics: Producer Extends Consumer Super
I do quite a lot of Java as part of the work I do at Gojek and one of the most helpful, but at times confusing part of the language is generics. Here I will be mentioning an interesting concept I recently came across — PECS or “Producer Extends, Consumer Super”
Generics in Java are invariant by default and are applied at compile-time. Polymorphic type arguments do not imply polymorphism in generic types, i.e., You cannot assign List<String>
to List<CharSequence>
.
However, with extends
and super
, you achieve “covariance” and “contra-variance” that aids in designing type-safe generic code aware of inheritance and polymorphism.
This is useful in defining methods that accept collection types.
- Use
? extends T
when the collection is “producing” items that you utilize. Like, iterating over all items of the collection. You only care if the list items are compatible withT
(i.e., subclasses ofT
). This is covariance — narrowing of scope. - Use
super
when the collection is “consuming” items. AcopyToList
method that copies current results into a list allows you to store objects of typeT
. This is contra-variance — widening of scope.
Examples
? extends T — covariance
You need to apply a List<Rule>
over a StateMachine
instance Here the List<Rule>
is a “producer.”
interface Rule {void apply(StateMachine m);}class FireWallRule implements Rule {// ...}class StateMachine {
// ...
public void apply(List<? extends Rule> rules) {
// ...
}
}class Main {
public static void main(String[] args) {
var m = new StateMachine();
var rules = List.of(
new FireWallRule(),
// ...
);
m.apply(rules);
}
}
This allows you to pass a List
of any objects which are implementations of Rule
? super T — contra-variance
You need to store some instances of FetchResult
, a sub-type of Result
into a List
. Here the List
is a “consumer.”
interface Result {
// ...
}interface FetchResult extends Result {
// ...
}class Fetcher {
putResult(List<? super FetchResult> output) {
// ...
}
}class Main {
public static void main(String[] args) {
var result = new ArrayList<Result>();
var fetcher = new Fetcher();
// ...
fetcher.getResult(result);
}
}
This allows to pass any List
that can store a FetchResult
.
However, such code wont work:
List<? extends CharSequence> list = new ArrayList<>();// ...list.add("hello world"); // error here, cannot add
However, a normal List<CharSequence>
will allow it.