Search code examples
javagenericsinheritancesyntaxsyntactic-sugar

Could Java recursive generics be seen as syntactic sugar for inheritance with overriding


I'm exploring the concept of recursive generics in Java and trying to understand its benefits over traditional inheritance and method overriding. Specifically, I want to know if recursive generics can be considered as "syntactic sugar" for a subclass with method overriding, I mean, are these two approaches functionally equivalent?

For example, consider a Builder pattern implemented with recursive generics:

public abstract class AnimalBuilder<T extends AnimalBuilder<T>> {
    private String name;

    public T setName(String name) {
        this.name = name;
        return (T) this;
    }

    public Animal build() {
        return new Animal(name);
    }
}

public class DogBuilder extends AnimalBuilder<DogBuilder> {
    private String breed;

    public DogBuilder setBreed(String breed) {
        this.breed = breed;
        return this;
    }

    public Dog build() {
        return new Dog(breed, super.build().getName());
    }
}

With this structure, I can chain method calls like:

Dog dog = new DogBuilder().setName("Fido").setBreed("Husky").build();

I can achieve a similar result using traditional inheritance and method overriding, by overriding setName return type within DogBuilder, although it would involve duplicating code in subclasses.

So my question is: could recursive generics be seen as just a more elegant and type-safe form of subclassing with method overriding? Or are there specific cases where recursive generics offer capabilities that subclassing and overriding cannot provide?


Solution

  • In this case of writing builders, having the recursive generic parameter (aka CRTP) is just to avoid lots of casting on the caller's side. That is, if you called one of the methods in the superclass first, you'd need to cast it to the subclass to continue building the subclass' fields, e.g.

    (
        (DogBuilder)new DogBuilder().setName("Foo") // setName() makes it an AnimalBuilder
    ).setBreed("Bar").build()
    

    I wouldn't call it "syntactic sugar" though. Syntactic sugar usually refers to language features that simply lowers your code. There is no lowering here. The compiler does enforce the generics here when it can. e.g. you can't pass a DogBuilder to something that expects AnimalBuilder<CatBuilder>.

    Also note that with CTRP, you must provide a generic type parameter when passing AnimalBuilders around. One could argue that this is a slight inconvenience.

    In general though, CRTP can do more than simple inheritance. It is basically the workaround for lack of a "self" type in Java. If you want a method to always take in an instance of "whatever subclass this is", you can do

    abstract class Animal<TSelf extends Animal<TSelf>> {
        public abstract TSelf makeBabiesWith(TSelf other);
    }
    

    This at least gives you some degree of compile time safety. People won't accidentally pass in something you don't expect (though they still can if they really wanted to). If you do this with simple inheritance,

    abstract class Animal {
        public abstract Animal makeBabiesWith(Animal other);
    }
    
    
    class Dog {
        public abstract Dog makeBabiesWith(Animal other) {
            // you must check this:
            if (other instanceof Dog) {
                // ...
            } else {
                throw new Exception(); // or something similar
            }
        }
    }
    

    People can accidentally pass in a non-Dog to Dog.makeBabiesWith and the compiler is completely fine about it.


    I also recommend looking at how lombok's @SuperBuilder works. It uses two recursive generic type parameters like this. One to represent the "current builder type" like you have shown, and another to represent "what this builder is building", to implement the toBuilder feature.