DEV Community

Neil Gall
Neil Gall

Posted on

Self-Referential Generics in Java

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> { ... }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ?
Enter fullscreen mode Exit fullscreen mode

It seems to be suggesting that the type bounds in Serial needs to be extends Part<P>, but if our application uses Serials for things other than Parts 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)));
    }
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
elmuerte profile image
Michiel Hendriks • Edited
class Serial<P> {
    // stores some identifier specific to P
}

interface Part<P> {
    Serial<P> getSerial();
}

This examples requires to be think about what it even means. Which puts it in the category of either: bad or, at best, avoid.

Collapse
 
neilgall profile image
Neil Gall

Agreed, and that's the easy part of all this code!