The point of this "manifesto" series is to go a bit more in-depth into fundamental generic concepts, and to share some subtle insights and possibly confusing gotchas.
The PECS principle, which stands for Producer-Extends-Consumer-Super, is a guideline to follow when using bounded wildcards.
Consider the classes Vehicle
, Car
, Bus
, and Roomba
, where Car
and Bus
extends Vehicle
and Roomba
extends Car
.
List<? extends Vehicle> garage = new ArrayList<>();
garage.add(new Vehicle()); // compilation error
garage.add(new Car()); // compilation error
garage.add(new Bus()); // compilation error
garage.add(new Roomba()); // compilation error
The errors above seem at first to be uncalled for -
garage
is a List of "any objects that extend Vehicle" (including
Vehicle itself), so it should be able to accept our Car, Bus,
and Vehicle objects.
What we're missing here is that List<? extends Vehicle>
doesn't actually mean a list that can contain any child objects of Vehicle.
List<? extends Vehicle>
is in fact saying that it is a
"list parameterized to one concrete class that extends Vehicle".
So List<? extends Vehicle>
is one of List<Car>
, List<Truck>
, or List<Roombar>
,
but the compiler does not know which. And because the compiler does not know which,
it does not allow the addition of, say, a Vehicle to List<? extends Vehicle>
- what if the
unknown identity of List<? extends Vehicle>
is List<Car>
? It wouldn't make
sense to add a Vehicle to a list of Cars. The same goes for adding a Car or Bus or Roomba
to List<? extends Vehicle>
.
What if instead of trying to add to garage, we only want to get from it?
List<? extends Vehicle> garage = new ArrayList<>(List.of(new Car(), new Bus()));
Vehicle vehicleOne = garage.get(0);
Vehicle vehicleTwo = garage.get(1);
We find ourselves knowing intuitively what type to declare vehicleOne
and vehicleTwo
as.
Since we are absolutely sure that garage is a list of a subtype of Vehicle, we know that
assigning its children to Vehicle will work.
It is in this sense that people say garage
is a "producer" of sorts - it produces values,
and we consume from it, but we can never insert values into it.
Applying this pattern into the context of a method would look something like this :
List<Vehicle> factory = new ArrayList<>();
public void addAllToFactory(List<? extends Vehicle> vehicles){
for(Vehicle oneVehicle : vehicles){
factory.add(oneVehicle);
}
}
List<Car> cars = new ArrayList<>();
cars.add(new Car());
addAllToFactory(cars); //works
The cars
that we pass into addAllToFactory
now takes on the role of a "producer",
yielding the values inside it for the factory
to "consume". And since it a "producer",
the vehicles parameter inside addAllToFactory
uses the corresponding wildcard-extends (PE in PECS).
A similar logic applies for Consumer-Super.
List<? super Car> garage = new ArrayList<>();
garage.add(new Car()); //runs
garage.add(new Roomba()); //runs
garage.add(new Vehicle()); // compilation error
The compiler knows that garage
references a supertype of Car
, but doesn't know which one.
I can add Roomba
and Car
to garage
because they're all subtypes of Car
, but Vehicle
needs to stay out. The type of garage
may or may not be a parent of Vehicle
- so in order to
avoid the possibility of a runtime error if garage turns out to be a subtype of Vehicle
, the compiler
plays it safe and throws a compilation error.
Adding stuff to garage makes it take on the role of a consumer; what if we want it to produce?
Car car = garage.get(0); //error
Car car = (Car)garage.get(0); //runs
Object object = garage.get(0); //runs
Because the compiler doesn't know the exact type of garage, it only accepts the assignment of a returned value
to an Object
, since all classes are Objects. If I want to assign it to something else, like Car
, then I
need to be sure that the first element of garage if of type Car
so I can downcast it. All these restrictions
makes wildcard-super a pretty poor producer.
An example of wildcard-super doing what it does best - acting as a consumer:
List<Car> cars = new ArrayList<>(List.of(new Car(), new Car()));
public void addCarsToFactory(List<? super Vehicle> factory){
for(Car car : cars){
factory.add(car);
}
}
List<Vehicle> factory = new ArrayList<>();
addCarsToFactory(factory); //works
The factory parameter in addCarsToFactory
is a consumer, so it uses super (CS of PECS).