How do you design a Builder that works across a class hierarchy so that subclass builder methods can be chained in any order while still returning the correct subtype?
answer
- Simulated self-type: Builder<T extends Builder<T>>
- abstract T self() → subclass returns this
- Every chaining method returns T, not the parent Builder
- Covariant build() per subclass (no cast)
- F-bounded recursive generic; reserve for real hierarchies
basics
~20 sMake the builder generic in its own type (a self type) and add an abstract self() method each subclass overrides to return this. Chaining methods return the self type, so even parent methods keep the subclass builder type, letting you call subclass methods afterward in any order.
solid answer
~50 sInheritance breaks naive builders: if a parent builder method returns the parent Builder type, you lose the subclass type and can no longer chain subclass-specific methods. The fix is the 'simulated self-type' idiom: declare the abstract parent builder as `Builder<T extends Builder<T>>`, have every chaining method return `T`, and add an abstract `T self()` that each concrete subclass builder implements as `return this`. Each parent method calls `self()` (or returns the result built from `self()`), so the static return type stays the concrete subclass builder regardless of call order. The abstract `build()` typically has a covariant return type so each subclass returns its own product. This lets clients write `new Pizza.Builder().addTopping(HAM).size(LARGE).build()` mixing parent and child methods freely. The cost is real generic complexity and boilerplate, so reserve it for genuine hierarchies; flat classes don't need it.
code
java · 30 linespublic abstract class Pizza {
public enum Topping { HAM, ONION, MUSHROOM }
final java.util.Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
java.util.EnumSet<Topping> toppings = java.util.EnumSet.noneOf(Topping.class);
public T addTopping(Topping t) {
toppings.add(java.util.Objects.requireNonNull(t));
return self(); // returns concrete subclass builder type
}
abstract Pizza build(); // covariant in subclasses
protected abstract T self(); // simulated self-type
}
Pizza(Builder<?> b) { toppings = b.toppings.clone(); }
}
public class NyPizza extends Pizza {
public enum Size { SMALL, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) { this.size = java.util.Objects.requireNonNull(size); }
@Override public NyPizza build() { return new NyPizza(this); } // covariant
@Override protected Builder self() { return this; }
}
private NyPizza(Builder b) { super(b); size = b.size; }
}
// Parent and subclass methods chain in any order; result is NyPizza, no cast:
// NyPizza pie = new NyPizza.Builder(Size.SMALL).addTopping(Topping.HAM).build();go deeper
Likely unfamiliar; should at least recognize that plain builders don't compose across inheritance and that there's a generic technique to fix it.
Understands the problem (subclass methods won't chain after parent methods) and can use such a builder, even if not author the recursive generics from scratch.
Can implement the simulated self-type builder: recursive bound, abstract self() returning this, T-returning chaining methods, covariant build(), and the two-level constructor copy.
Explains the F-bounded generic and self-type simulation precisely, weighs the complexity/readability cost against the benefit, sets guidance on when a hierarchy builder is justified, and can articulate alternatives (composition, separate builders) and their trade-offs.
## The problem inheritance creates Suppose `Pizza` is an abstract base with subclasses `NyPizza` and `Calzone`, and you want a Builder for each. A subclass builder must offer both the **parent's** chaining methods (e.g. `addTopping(...)`) and its **own** (e.g. `size(...)` for `NyPizza`). With a naive design, the parent builder's `addTopping` returns the *parent* `Builder` type. So after calling it, the compiler thinks you hold a parent builder and **forbids** calling subclass methods — unless you carefully order every parent call last. Chaining in arbitrary order becomes impossible, and downcasting is ugly and unsafe. ## The simulated self-type idiom Java lacks a true *self type* (a way to say 'the exact runtime type of this'), so Effective Java uses a generic workaround. The abstract parent builder is parameterized by its own subtype: ```java public abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping t) { toppings.add(Objects.requireNonNull(t)); return self(); // <-- returns the subclass builder type } abstract Pizza build(); // covariant in subclasses protected abstract T self(); // each subclass returns 'this' } ``` Key pieces: - **`<T extends Builder<T>>`** — the *recursive (F-bounded) generic*. It binds `T` to the concrete subclass builder type. Read it as 'T is some builder whose self-type is T.' - **`abstract T self()`** — the simulated self-type hook. Each concrete subclass implements it as `return this;`, which the compiler knows has type `T` because the subclass declares itself as that `T`. - **Every chaining method returns `T`**, not the raw parent `Builder`. So even when you call the *parent's* `addTopping`, the static type you get back is the *subclass* builder, and you can keep chaining subclass methods. - **`abstract Pizza build()`** with a **covariant return type** in each subclass: `NyPizza.Builder.build()` returns `NyPizza`, not just `Pizza`, so callers get the precise product without casting. ## A concrete subclass ```java public static class NyPizza extends Pizza { 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); } // covariant @Override protected Builder self() { return this; } // self-type } private NyPizza(Builder b) { super(b); /* copy size */ } } ``` The parent's constructor takes the parent `Builder<?>` and copies shared fields; the subclass constructor takes its own `Builder` and copies subclass fields after `super(builder)`. ## Why it works (order independence) Because `addTopping` returns `T` = `NyPizza.Builder`, this compiles regardless of order: ```java NyPizza pie = new NyPizza.Builder(SMALL) .addTopping(SAUSAGE) // parent method, but returns NyPizza.Builder .size(LARGE) // wait: size was the ctor arg here; e.g. .addTopping(ONION) .build(); // returns NyPizza ``` You can interleave parent and subclass calls freely; the static type never degrades to the parent builder. ## Terms defined - **Self type:** a type that always refers to the exact class of `this`, even in subclasses. Java has no built-in self type, hence the simulation. - **F-bounded / recursive generic:** a type parameter bounded by an expression that mentions itself, `T extends Builder<T>`. It's the standard trick to approximate a self type. - **Covariant return type:** an overriding method may return a *subtype* of what the overridden method returns (allowed since Java 5). Here each subclass `build()` narrows `Pizza` to its own product type. ## Cost / when to use This is powerful but heavy: recursive generics, an abstract `self()`, covariant builds, and a two-level constructor copy. Use it **only for real builder hierarchies** where subclasses genuinely extend a common parameter set. For flat classes, a plain Builder is simpler and clearer; over-engineering a hierarchy-builder where there's no hierarchy is a smell.
- What does the recursive type bound 'T extends Builder<T>' buy you?It binds T to the concrete subclass builder, so parent chaining methods can return T (the subclass builder type) rather than the raw parent Builder. That preserves the subclass type through every call, letting subclass-specific methods chain in any order without casts.
- Why is a covariant return type on build() useful here?Each subclass builder's build() can return its own product type (e.g. NyPizza instead of Pizza), so callers receive the precise type and don't need to downcast. Covariant returns have been legal since Java 5.
- When is this generic builder overkill?For flat classes with no inheritance. The recursive generics, self() hook, and covariant builds add real complexity that only pays off when you genuinely have a builder hierarchy sharing a common parameter set.
It's like a relay baton that always knows which exact runner is holding it. No matter how many shared (parent) legs of the race you run, the baton reports your specific team, so you can still hand off to your team's specialist (subclass method) at any point — instead of being demoted to 'generic runner' after a shared leg.
saying these in an interview costs you the question
- Returning the parent Builder type from parent chaining methods — subclass methods then won't chain without casts.
- Downcasting the builder between calls instead of using the self-type idiom.
- Forgetting the abstract self() (or not returning 'this' from it), breaking the type-preservation chain.
- Applying the recursive-generic hierarchy builder to a flat class with no subclasses (needless complexity).