Search code examples
javagenericsdesign-patternsbuilder

how generic type with a recursive type parameter along with the abstract self method allows method chaining to work properly?


I am reading Effective Java Edition 3. in chapter 2 page 14, the author talks about the builder pattern and presents this code:

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

an implementation of above abstract class:

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        @Override protected Builder self() { return this; }
    }
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

and we can use code like this:

NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();

quote from the book:

Note that Pizza.Builder is a generic type with recursive type parameter. this, along with the abstract self method, allows method chaining to work properly in subclasses, without the need for casts.

now my question is what power/value <T extends Builder<T>> added to Pizza class and how it is different with <T extends Builder>? if you are going to explain <T extends Builder<T>> to a five years old kid in simple English how would you explain it?

and I can't figure out the purpose of abstract self method in the super class?


I add this part because of the comment section

Imagine I have changed the above codes like this(it is not going to be the best example just for illustration purposes):

I changed the NyPizza.Builder to be generic:

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder<T> extends Pizza.Builder<Builder<T>> {
        private final Size size;
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        @Override protected Builder self() { return this; }
    }
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

and the Pizza class like this:

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    abstract static class Builder<T extends Builder> {
        T obj;
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        public T builder(){
            return obj;
        }

        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

and used the above classes like this:

NyPizza.Builder<String> test = new NyPizza.Builder<String>(SMALL).builder();

now because I didn't define class Builder<T extends Builder> in this form: class Builder<T extends Builder<T>> the builder method in the Pizza class should not be able to detect the type of the T is NyPizza.Builder<String> but it can. how is it possible? how change it to need casting?


Solution

  • Imagine a class:

    class Foo<T extends Foo<T>> {
      T foo() { ... }
    }
    

    What this is saying is that the foo() method, in the Foo<T> class, will return an instance of something that's also a Foo<T>, because all possible types represented by T are subclasses of Foo<T>.

    So it can return itself - which is why it's useful for method chaining with builders; but there's actually nothing that stops it returning some other class within the bound.

    If you were to declare it as:

    class Foo<T extends Foo> {
      T foo() { ... }
    }
    

    then this makes T a raw-typed Foo: foo() returns a Foo, not a Foo<T>. Because raw types erase all generics, this means that in a call chain like foo().foo(), you are losing type information after the first call.

    In many situations, this may not feel super-important. But raw types should be avoided in pretty much all situations. The consequences of a raw type here would be:

    1. If you weren't returning the self type (alluded to above with the "there's actually nothing that stops it returning some other class"), you would lose that "other class" after the first method call.
    2. If you had other generic methods in Foo, e.g. List<String> myList() { ... }, the raw-typed Foo would return raw List, not List<String> (raw types erase all generics, not just the ones related to the omitted type variables).

    These clearly don't apply in all situations; but since it is simply bad practice to introduce unnecessary raw types, make sure you don't introduce them.