Switching a class from `extends Base` to merely holding a `Base` as a field takes away more than the inherited methods: a subclass can also read and write the base's `protected` fields (Java's `java.util.Vector` declares `elementData` and `elementCount` that way), while a containing object can only call the base's public API. What does a base class share with its subclasses beyond its public API, why is that sharing almost impossible to take back later, and how do Smalltalk, Java/C++, Scala traits, Go embedding and Rust traits differ in what state a reuse mechanism can carry?
answer
- Inheritance shares data; delegation shares only API
- protected = second API, never narrowable
- Vector's elementData froze the representation forever
- Scala trait val + linearization = default value
- Rust traits carry no fields; Go promotes fields, not identity
basics
~20 sInheritance shares state, not just code. Protected fields are a second, subclass-facing API you can never narrow, and they freeze the base's representation. Containment reuses no state unless the base publishes accessors — that is the real cost of extend-to-contain.
solid answer
~60 sExtending gives a subclass three things: the base's public API, its method bodies, and — uniquely — direct access to its **data**. Delegation replaces the first two; nothing replaces the third. So `protected` fields are a second API with a second audience, and it can never be narrowed. `java.util.Vector` still declares `elementData`/`elementCount` protected, which freezes its representation permanently and lets any subclass corrupt invariants the class is supposed to maintain. The divergence: Smalltalk offers no choice — every instance variable is subclass-visible by construction. Java/C++ make it opt-in (C++ additionally forbids reaching a protected member through a base-typed reference). Scala traits may declare concrete `val`s, assigned in linearization order, so a trait initialized earlier reads another's field as `null`/0 — hence `lazy val` and Scala 3 trait parameters. Rust traits hold no data at all: default methods reach state only through required accessors, which is why delegation macro crates exist. Go embedding promotes fields but the base's own methods never see the outer type. So extend→contain is a base-side refactor: if the subclass touched protected state, you must widen the base's public API to do it.
code
text · 14 linesclass List: # base, version 1
protected buf: Array # data shared with every subclass
protected count: Int
class Stack extends List:
push(x): buf[count] = x; count = count + 1 # touches base data directly
# version 2 wants a chunked representation instead of one array.
# impossible: subclasses you do not own index buf and assign count.
# smaller commitment: keep the data private, share only operations
class List:
private buf, count
protected slotFor(i): ... # representation is now free to changego deeper
Know the core fact: extending shares the base's data, not only its methods, and protected fields are visible to every subclass. Be able to say why a wrapper object cannot reach them.
Explain why protected is a versioning commitment that freezes the representation, name Vector's elementData as the concrete case, and describe the accessor-method alternative and what it still costs.
Diagnose it in a real codebase: find which protected state subclasses actually touch, and show that extend-to-contain requires widening the base's public API. Bring at least two named-language contrasts (Rust's required accessors, Scala's linearization hazard, Go's field promotion without identity).
Frame it as an API-boundary policy: what a library commits to when it publishes a protected member, whether classes should be sealed or final by default, and how the choice differs for internal code you can atomically refactor versus a published base. Note that the escape hatches are all base-side and unavailable to consumers.
## The third thing inheritance gives you When a class extends another it acquires three separable things. First, the base's **public API**, which becomes part of the derived type — this is what the classic reuse critique is about (a stack built by extending a random-access list is also indexable in the middle). Second, the base's **method bodies**, the code reuse people say they wanted. Third, and least discussed, **direct access to the base's data**: the fields the base declares `protected` in Java, C++, C# or Kotlin, or in some languages every field it has. Every composition mechanism replaces the first two. None of them replaces the third. A wrapper object holding a `Base` can call `Base`'s public methods; it cannot see `Base`'s private fields, and no amount of forwarding invents that access. That asymmetry is why "just use composition" is cheap advice on a whiteboard and expensive in a codebase. ## `protected` is a second public API A published `protected` field has an audience you cannot enumerate: every subclass anyone ever writes. It therefore obeys the same versioning rules as a public member — you can widen it, never narrow it, never remove it, never change its type or meaning. `java.util.Vector` declares `elementData`, `elementCount` and (via `AbstractList`) `modCount` as protected. That decision permanently fixed the representation: Vector can never become a chunked or paged structure, because external subclasses index `elementData` directly. `Stack extends Vector` inherits both the polluted public API and this frozen representation. Worse, a mutable protected field means the base can no longer maintain its own invariants. Anything a subclass can write, the base must treat as untrusted — but base methods don't re-validate, so the honest statement is that the invariant simply isn't enforced. `protected` doesn't just leak the representation; it hands out write access to it. Two details people get wrong. In Java, `protected` also implies package access, so it is *wider* than "subclasses only". In C++, a derived class may only reach a protected non-static member through an object of its own type — `Base& b; b.protectedField;` inside `Derived` is ill-formed, while `Derived& d; d.protectedField;` is fine. Kotlin's `protected` is narrower than Java's: subclasses only, no package rule. ## Five languages, five answers about shared state **Smalltalk** takes the choice away. Instance variables are visible to `self` and to every subclass, always; there is no way to declare one private to the defining class. (Symmetrically, no object can touch *another* object's instance variables at all, not even one of the same class — unlike Java's per-class `private`.) So in Smalltalk every subclass is coupled to the superclass's representation by construction, and the discipline has to live in convention and accessor methods. **Java and C++** make it opt-in with `protected`, which is exactly the trap: it looks like a small local decision and is actually a permanent API commitment. **Scala traits** can declare concrete `val`s and `var`s, so a trait carries state. The cost is initialization order: trait fields are assigned in linearization order, so a trait constructed earlier that reads a field declared in a trait constructed later observes the type's default — `0`, `false`, `null` — with no warning. The Scala 2 workarounds are `lazy val`, turning the member into a `def`, or early definitions; Scala 3 replaced that machinery with trait parameters. Stateful mixins are possible, but the state has a construction-time hazard that plain fields in a class do not. **Rust traits cannot hold data at all.** A default method can only call other methods of the trait. Shared state is therefore reached through a *required accessor* the implementor supplies — commonly `fn inner(&self) -> &Shared` plus `fn inner_mut(&mut self) -> &mut Shared` — or by holding the struct as a field and writing forwarders, which is why delegation macro crates exist. The upside is that the state dependency is written down in the trait's signature; the downside is per-implementor boilerplate and borrow-checker friction when a default method needs two parts of the state mutably. **Go embedding** shares fields: an embedded struct's fields are promoted to the outer type, so the outer type reads and writes them directly. What it does not share is identity — a promoted method runs with the *embedded value* as receiver, so a base method calling another method never dispatches to an outer redefinition. You get inheritance's state coupling without inheritance's open recursion. ## What the alternatives actually cost Replace protected fields with protected accessor methods: representation freedom comes back and the setter can validate, but you still have a second API and still have a fragile base class. Extract the shared state into its own object that base and derived both hold: this genuinely converts inheritance into composition, but it is a change to the *base*, so it's unavailable to anyone who doesn't own it. And plain contain-and-delegate only works if everything the subclass touched is already public — if it read a protected field, the refactor requires widening the base's public API, the exact opposite of the encapsulation win you were chasing. C++ private inheritance and Eiffel's non-conforming inheritance drop the subtype claim but keep the data access, which is why they remain the pragmatic escape hatch. Deep hierarchies compound all of this: state introduced at depth 2 is readable and writable at depths 2 through 5, and no single class in the chain can state, let alone enforce, an invariant over it.
- You own the base class. How would you actually remove a published protected field without breaking subclasses?You can't do it silently — the field is part of the compatibility surface. The staged move is to add protected accessor methods with the same semantics, migrate the subclasses you control to them, then either delete the field in a breaking major release or, if the base ships to outside consumers, seal the class so no new subclasses appear and keep the field as a shim for existing ones. The real lesson is preventive: default fields to private and expose the smallest protected surface you are willing to support forever.
- Rust traits can't hold fields. Doesn't that just push the same coupling into required accessor methods?It pushes it, but into a place with better properties. The dependency is written into the trait's signature, so both sides see exactly what state the default methods need, and each implementor decides how to supply it — a real field, a computed value, or a projection into something larger. What you lose is the free ride: every implementor writes the accessor, and a default method needing two disjoint pieces mutably has to fight the borrow checker in a way a protected field never would.
- Go embedding promotes the embedded type's fields. Is that inheritance?It reproduces inheritance's state coupling and its API pollution — promoted fields and methods are reachable on the outer type, and the outer type satisfies the embedded type's interfaces — but not its identity. A promoted method runs with the embedded value as receiver, so it never dispatches back to a method redefined on the outer type. You get the fragile-base-class data coupling without open recursion, which removes one hazard and keeps the other.
Public inheritance hands the new tenant a key to the wiring closet, not just the flat. You can change the furniture layout later; you can never re-run the wiring, because half the building is now spliced into it.
saying these in an interview costs you the question
- Saying protected members are implementation detail you can change freely — they are a permanent API to every subclass ever written.
- Claiming composition can always replace inheritance mechanically, ignoring that a wrapper cannot see the base's private data.
- Believing Java's protected means subclasses only; it also grants package access.
- Assuming Scala trait vals are initialized before any constructor body runs, rather than in linearization order.
- Saying Rust traits can carry shared state through supertraits — supertraits carry no fields either.
- Treating Go embedding as safe composition because it looks like a field; embedded fields and methods are promoted just like inherited ones.