In his book Effective Java, Joshua Bloch coined the pneumonic device PECS: Producer Extends, Consumer Super. This device can help us remember which Java keyword (extends
or super
) to use when specifying the bounds on generic types.
Let's say that we have some generic boxes that can hold whatever we want.
class Box<T> { ... }
Let's say that we also have some operations that we want to perform on these boxes. We implement these actions using a visitor, parameterized on the type of data contained in the host box U and the result type of the operation R.
class BoxVisitor<U, R> { ... }
We'll assume three classes of objects we might put in our boxes.
![]() Fuel |
> | ![]() Plants |
> | ![]() Bamboo |
Above, the > sign designates a substitutability relationship. Since Fuel > Plants, we can use Plants anywhere that simply requires a Fuel. This is because plant matter is a type of fuel. Similarly, since Bamboo is a type of plant, we can use Bamboo anywhere that requires Plants or Fuel; therefore, Plants > Bamboo and Fuel > Bamboo. This is the subtype relationship that we're so familiar with.
We'll also assume three classes of objects that might want to do something with our boxes (i.e. our visitors).
![]() Consumer of anything that burns |
< | ![]() Consumers of plants |
< | ![]() Consumers of bamboo |
Interestingly, the substitutability relationship goes in the opposite direction here. Although a panda is a type of herbivore, we see that Herbivore < Panda. This is because, although pandas are a subset of the herbivores, the panda's dietary restrictions prevent us from substituting it for a general consumer of all plants. However, since the class herbivores should be able to eat any kind of plant, we could substitute a consumer of plants anywhere that we need a consumer of bamboo. The class declaration for pandas might look like the following.
class Panda<R> extends BoxVisitor<Bamboo, R> { ... }
Boxes are hosts. Hosts are producers because they just provide the data for some action by a visitor. Therefore, host types should use the extends
keyword to create an upper-bounded generic type (remember: Producer Extends). An upper-bounded type parameter implies that this is a covariant relationship.
if you need to feed a... |
you can give it a... | ||
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|
![]() |
![]() |
class BoxVisitor<U, R> { public R visit(Box<? extends U> host) { // ... } }
Our BoxVisitors are consumers because they consume the data provide by the host and apply some action. Therefore, visitor types should use the super
keyword to create a lower-bounded generic type (remember: Consumer Super). A lower-bounded type parameter implies that this is a contravariant relationship.
if you have a... |
you can give it to... | ||
---|---|---|---|
![]() |
![]() |
||
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
![]() |
class Box<T> { public <R> R accept(BoxVisitor<? super T, R> visitor) { // ... } }
The images in this document are licensed under the Creative Commons Share-Alike License. The producer/consumer figures were adapted from a wonderful diagram by Andrey Tyukin.