I've been having an ongoing debate with colleagues about the use of self-referential generics in Java, in which I assert they are unsound, and others continue to believe in their use. By self-referential generics I mean a type defined in terms of itself, e.g.:
interface Foo<T> { ... }
interface Bar extends Foo<Bar> { ... }
On the surface this appears useful as it can enforce correct typing of parallel hierarchies. For example, consider a warehouse system which deals with various types of parts. Each part has a type-specific identifier:
class Serial<P> {
// stores some identifier specific to P
}
interface Part<P> {
Serial<P> getSerial();
}
Whenever we define a part class, we declare that it implements Part
parameterised with the class itself. This enforces that the serial gets the same type:
class Whatsit implements Part<Whatsit> {
private final Serial<Whatsit> serial;
@Override
public Serial<Whatsit> getSerial() {
return serial;
}
}
We can even have a part database which uses a polymorphic method to ensure if you ask for a Whatsit
you only get a Whatsit
.
interface PartDatabase {
<P extends Part<P>> P getPart(Serial<P> serial);
}
So far this is all sound, and we can even work with parts generically in this way:
class HomogenousPartCruncher<P extends Part<P>> {
private final PartDatabase database;
void crunchParts(List<Serial<P>> serials) {
serials.forEach(serial -> crunch(database.getPart(serial)));
}
void crunch(Part<P> part) {
// TODO
}
}
Any given instance of HomogenousPartCruncher
can only work with one type of Part
however, the type it is instantiated with. The clue's in the name. But as soon as we try to use an Object Oriented style of coding it all falls apart:
class HeterogeneousPartCruncher {
private final PartDatabase database;
void crunchParts(List<Serial<? extends Part<?>>> serials) {
serials.forEach(serial -> crunch(database.getPart(serial)));
}
void crunch(Part<?> part) {
// TODO
}
}
We don't know the type of each Serial
in the list but it's at least a Part<?>
. We can therefore try to get the actual Part<?>
from the database using this serial. But the compiler complains with
error: method getPart in interface PartDatabase cannot be applied to given types;
serials.forEach(serial -> crunch2(database.getPart(serial)));
^
required: Serial<P>
found: Serial<CAP#1>
reason: inference variable P has incompatible bounds
equality constraints: CAP#2,CAP#1
upper bounds: Part<P>
where P is a type-variable:
P extends Part<P> declared in method <P>getPart(Serial<P>)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Part<?> from capture of ? extends Part<?>
CAP#2 extends Object from capture of ?
It seems to be suggesting that the type bounds in Serial
needs to be extends Part<P>
, but if our application uses Serial
s for things other than Part
s we can't do this. It also creates a cyclic dependency which is never an improvement to your overall code structure.
The only way to fix this is to pretend that Part
is not generic at all, and implement crunchParts()
like this:
@SuppressWarnings("unchecked")
void crunchParts(PartDatabase database, List<Serial<? extends Part>> serials) {
serials.forEach(serial -> crunch(database.getPart(serial)));
}
And in fact everywhere you refer to Part
in relation to this code, it has to be Part<?>
, including return types which is a terrible code smell. Part
may as well not be generic at all:
interface Part {
Serial<? extends Part> getSerial();
}
class Whatsit implements Part {
@Override
public Serial<Whatsit> getSerial() { ... }
}
}
interface PartDatabase {
<P extends Part> P getPart(Serial<P> serial);
}
class HomogenousPartCruncher<P extends Part> {
private final PartDatabase database;
void crunchParts(List<Serial<P>> serials) {
serials.forEach(serial -> crunch(database.getPart(serial)));
}
void crunch(P part) {
// TODO
}
}
class HeterogeneousPartCruncher {
private final PartDatabase database;
void crunchParts(List<Serial<? extends Part>> serials) {
serials.forEach(serial -> crunch2(database.getPart(serial)));
}
void crunch2(Part part) {
// TODO
}
}
The code is cleaner and easier to understand, has the same guarantees of type safety, yet doesn't need <?>
wildcards nor @SuppressWarnings("unchecked")
everywhere.
The only way to use self-referential generics without annoying wildcards and suppressing unchecked cast warnings is for all - and I mean all - your code to work with concrete types only. Which is a distinctly non-object oriented style that I find curious in a language so singularly devoted to promoting Object Oriented programming that class hierarchies are pretty much the only abstraction tool it offers.
Top comments (2)
This examples requires to be think about what it even means. Which puts it in the category of either: bad or, at best, avoid.
Agreed, and that's the easy part of all this code!