skip to content

Inheritance & IS-A

Deriving a specific type from a general one: hierarchies, overriding, and the rule that a subtype must work wherever its supertype does. Interviewers probe where reuse-driven inheritance breaks.

on this pageshow

questions

10

Inheriting an implementation and being usable wherever another type is expected are two different relationships. Which languages let you have one without the other, and what is each mechanism actually for?

level: middleimportance: must knowfreq 55%

answer

  1. subtyping = substitutable at use sites; subclassing = where the code came from
  2. C++ public = is-a, private = implemented-in-terms-of
  3. Go: embedding for reuse, structural interfaces for subtyping, unrelated
  4. Rust: no implementation inheritance; default trait methods carry the reuse
  5. Java `extends` grants both, so reuse leaks a promise

basics

~20 s

Subclassing is code reuse; subtyping is substitutability. C++ private inheritance and Eiffel non-conforming inheritance give reuse with no substitutability; Go embedding and Rust default trait methods give reuse without inheritance; Go and TypeScript grant subtyping with no declaration at all.

solid answer

~50 s

They are separate relations that Java and C# happen to weld together in one keyword. - **C++** separates them in the language: `class Stack : private Deque` inherits every member but creates no implicit conversion to `Deque&`, so the compiler enforces "implemented in terms of" while denying "is-a". Only public inheritance is subtyping. - **Eiffel** does the same with non-conforming inheritance (`inherit {NONE}`) -- explicit reuse that carries no conformance claim. - **Go** has no inheritance at all: struct embedding forwards methods (reuse), interfaces are satisfied structurally (subtyping), and embedding a type never makes the outer type usable where the embedded one is expected. - **Rust** has no implementation inheritance either; traits supply polymorphism, and default method bodies supply the reuse that a base class would have. - **Java/C#**: `extends` grants both at once, so inheriting for reuse silently publishes a substitutability promise you never meant to make.

code

cpp · 9 lines
cpp
class Deque { public: void push_front(int); void push_back(int); };

class Stack : private Deque {          // subclassing, NOT subtyping
public:
    void push(int x) { push_back(x); } // reuse of the inherited member
};

void consume(Deque& d);
// consume(s);  // rejected: no implicit conversion from Stack to Deque

go deeper

for a junior

State both relations in plain words -- substitutable at use sites versus where the code came from -- and give interface implementation as the everyday example of subtyping without shared code.

for a middle

Name a language that separates them by keyword and one that has no implementation inheritance at all, and explain what each mechanism is for.

for a senior

Discuss the consequence for published APIs: in a welded language every base you extend becomes part of your contract, and reuse-only relationships need delegation.

for a principal

Frame it as a boundary-design decision -- which relations your platform's public types are allowed to assert, and whether the language's default forces you to over-promise.

## Two relations, often confused because one keyword grants both **Subtyping** is a statement about *use sites*: wherever a value of type B is expected, a value of type A may appear. It is about the interface a caller sees and the behaviour it may rely on. **Subclassing** is a statement about *construction*: this class obtains members -- fields, method bodies, sometimes constructors -- from another class. It is a code-sharing mechanism. They are logically independent. A type can be a subtype of another with no shared code (two unrelated classes implementing the same interface). A type can share code with another and not be a subtype (a stack built out of a deque's internals). The reason candidates confuse them is that `extends` in Java, C# and many others grants both in one stroke, so the only spelling for reuse also publishes a substitutability claim. ## Languages that separate them explicitly **C++** is the clearest case, because the separation is a keyword. `class Stack : public Deque` is subtyping plus subclassing: a `Stack&` converts to `Deque&`. `class Stack : private Deque` is subclassing only: members are inherited and usable inside `Stack`, but no implicit conversion exists, so no caller can substitute. The C++ community's own guidance is exactly this reading -- public inheritance means is-a, private inheritance means implemented-in-terms-of, and the latter is a slightly more powerful form of composition (it permits overriding a virtual and accessing protected members) at the cost of tighter coupling. **Eiffel** offers non-conforming inheritance: `inherit {NONE} SOME_CLASS` reuses without conformance. That Eiffel -- a language that treats inheritance as its universal structuring tool -- needed such a feature is itself the argument that the relations are distinct. **Go** removes implementation inheritance entirely and then supplies both jobs separately. Struct embedding promotes the embedded type's methods onto the outer type, which is reuse by delegation with syntax support. Interface satisfaction is structural and undeclared: a type satisfies an interface by having the methods. Critically, embedding `Reader` into `MyThing` does *not* make `MyThing` assignable to a `Reader` variable unless it also happens to have the methods the interface requires -- and when it does, that is the structural rule doing the work, not the embedding. **Rust** likewise has no implementation inheritance. Traits give the polymorphism, and default method bodies in a trait give the reuse a base class would have provided; supertraits express requirement, not code sharing. When you genuinely want to reuse an existing type's data layout, you compose or wrap. **TypeScript** flips the other axis: subtyping is structural, so a class that never mentions an interface still satisfies it, and two structurally identical classes are mutually assignable. Declaring `private` or `#` fields brands a class so that structural twins stop being interchangeable -- an opt-in back into nominality precisely because pure structure is too permissive for domain types. ## Why the distinction earns its keep Once you can name the two relations, several everyday decisions stop being matters of taste. - **"Should this extend that?"** becomes two questions: do I want the code, and do I want callers to substitute? If the answers differ, the language tells you the tool. In C++ that is private inheritance or a member; in Go, embedding; in Java, a field plus delegation, because Java cannot express reuse-without-subtyping. - **Published API surface.** In a language that welds the relations, every base class you extend for convenience becomes part of your type's public contract, including its protected members and its future changes. - **Interface inheritance is the cheap half.** Implementing an interface commits you to a signature set and nothing else; there is no inherited state, no constructor ordering, and no surprise when the parent changes. ## The trap to avoid in an interview Do not answer "just use composition". That is a conclusion, and it is one this leaf's sibling material argues at length. The question here is about the *relations*: which one a language checks, which one it grants, and which mechanism supplies each. A candidate who says "C++ private inheritance gives me the members without the conversion, Go embedding gives me the methods without any conformance claim, and Java gives me no way to separate them" has demonstrated the concept. A candidate who says "prefer composition" has repeated a slogan. ## A note on what the compiler is actually checking When a compiler accepts a subtype claim, it checks *shape*: member names and signatures, plus variance rules where they apply. It does not check that the subtype behaves like the supertype -- that is the discipline named by the Liskov Substitution Principle, and no mainstream compiler verifies it. So even the relation the type system does track is tracked only structurally; the behavioural half of "is-a" remains a claim you make and must test. ## Interview shape Define both relations in one sentence each, give one language that separates them by keyword (C++), one that separates them by having no inheritance at all (Go or Rust), and note that welding them together is why "inherit to reuse" leaks a promise.

  • Java has no private inheritance. If you want a base class's code but not its substitutability, what do you actually do?
    Hold the would-be base as a private field and delegate the methods you want to expose -- composition with forwarding. You lose the ability to override the base's virtual methods and to touch its protected members, which is exactly what C++ private inheritance preserves, so the Java version is slightly weaker but also more decoupled. Making the wrapper implement a narrow interface of its own restores polymorphism where callers genuinely need it.
  • Go's embedding promotes methods onto the outer type. Doesn't that make the outer type substitutable for the embedded one?
    No. Promotion is a method-set operation, not a conversion: `Stack` gains `PushBack` but a `*Deque` parameter still rejects a `*Stack`. What promotion can do is make the outer type satisfy an *interface* that the embedded type satisfied, because the promoted methods are in its method set -- so substitutability appears only through the structural interface rule, and only for the interface, never for the concrete embedded type.
  • TypeScript accepts a class as an implementation of an interface it never mentions. When is that a problem, and what is the standard fix?
    It is a problem when two types share a shape but not a meaning -- a `UserId` and an `OrderId` that are both `{ value: string }` become interchangeable, and a transposition bug type-checks. The standard fix is branding: give each type a `private` or `#` field, or a phantom literal property, so structural comparison fails and the type behaves nominally. That is opting back into declaration-based conformance for the cases where shape is not identity.

Subtyping is being accepted at a checkpoint because you carry the right credentials; subclassing is having been trained at the same academy. Some organisations issue credentials on graduation and the two look identical -- until someone trains there and is deliberately denied the badge.

saying these in an interview costs you the question

  • Answering only "prefer composition over inheritance" -- a conclusion, not the distinction the question asks for.
  • Claiming Go embedding makes the outer type usable wherever the embedded type is expected.
  • Believing every language grants both relations with one keyword, so the distinction is only theoretical.
  • Saying interface implementation is "inheritance without code" and stopping there, missing that structural languages need no declaration at all.

context

open as a page

In Go, a struct that embeds another struct gets that struct's methods, yet a promoted method never calls back into a method the outer struct redefines. Name the mechanism Go is missing here, explain why that mechanism is what makes base classes fragile in other languages, and say what Go gives up by not having it.

level: middleimportance: should knowfreq 30%

basics

~20 s

Go embedding has no open recursion: inside a promoted method, the receiver is still the embedded value, so redefining a method in the outer struct shadows rather than overrides. Open recursion — late-bound self-calls — is what turns a base class's internal call graph into part of its contract. Go gives up the template method pattern.

open as a page

When an override calls "the parent version" of a method, languages disagree about which method that actually is. Compare how Python's super(), Ruby's super, Scala's stackable traits and C++'s explicit base qualification pick the target, and what breaks if you assume it is always the lexically named base.

level: middleimportance: should knowfreq 38%

basics

~20 s

Python, Ruby and Scala resolve the parent call dynamically along the receiver's linearized ancestor list, so the target can be a class the caller never mentions. C++ has no super and forces a statically named base. Java, C# and Kotlin bind super to one fixed base.

open as a page

Some languages let a type declare that only a fixed, known set of types may extend or implement it — Java's `sealed ... permits`, Kotlin's `sealed class`, Scala 3's `sealed trait`, Rust's `enum`. What does a compiler gain from that guarantee, and where do these languages actually draw the boundary of "known"?

level: middleimportance: should knowfreq 45%

basics

~20 s

It gains exhaustiveness: the compiler can enumerate the cases, so a match needs no default branch. Where "known" ends differs — the type itself in Rust and Haskell, the file in Scala 3, the package plus module in Java and Kotlin.

open as a page

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?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Inheritance 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.

open as a page

A library publishes a closed set of subtypes in v1, downstream code matches exhaustively over it, and v2 adds one more member. What actually breaks, and how do languages differ in what they force the library author or the client to write?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Adding a case breaks every exhaustive match. Recompiled clients get a compile error; un-recompiled ones fail at run time -- Java throws MatchException, Kotlin NoWhenBranchMatchedException. Rust's #[non_exhaustive] and Swift's non-frozen enums pre-charge clients a catch-all arm to make the addition non-breaking.

open as a page

Saying that one type is a subtype of another asserts that its values behave acceptably anywhere the supertype is expected — not merely that the members line up. Which real languages and tools actually try to check that behavioural assertion mechanically, how far does each one get, and what do teams do on a stack where nothing checks it at all?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Compilers check shape, not behaviour. Go and TypeScript infer the claim from a matching shape alone; Java and C# accept covariant arrays and repair them at run time; only proof tools like SPARK and OpenJML genuinely verify; Rust's unsafe impl admits the compiler cannot. Otherwise, run one shared conformance suite against every implementer.

open as a page

Java makes methods overridable by default, while C++, C# and Kotlin require the base author to opt in with virtual or open. Argue both positions: what does opt-in overridability buy, and what does it cost consumers of a library who need an extension point the author did not provide?

level: principalimportance: should knowfreq 26%

basics

~20 s

Opt-in overridability (C++ virtual, C# virtual, Kotlin open) makes the base author declare every extension point, so nothing is accidentally part of the contract. It costs downstream users adaptability: what the author did not open cannot be adapted, mocked or wrapped, so they extract interfaces, fork, or use bytecode tricks.

open as a page

The phrase "fragile base class" is used for two different failures — one about behaviour and one about compiled binaries. Distinguish them, say which platforms suffer which, and explain why the mitigations do not transfer between the two.

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Semantic fragility: a base changes its internal self-call structure and previously-correct subclasses misbehave. Binary fragility: adding a field or virtual to a C++ base shifts object layout and vtable offsets, invalidating already-compiled subclasses. Java, C# and the modern Objective-C runtime resolve layout at load time and avoid the second, not the first.

open as a page

Go and Rust shipped without implementation inheritance at all. What replaces it, what does a team actually pay in code that would have used a three-level class hierarchy, and what does the omission buy?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Rust replaces it with trait default methods plus generics or dyn Trait; Go with struct embedding plus implicitly satisfied interfaces. Both share behaviour but not state, so teams pay forwarding boilerplate and lose subclass hooks. They buy no fragile base coupling, no diamond, and easier addition of new operations.

open as a page