skip to content

Abstract Type vs Interface

A pure behavioral contract versus a partially implemented base that carries shared state and defers steps. Interviewers use the choice to see whether you reason about substitutability or only reuse.

on this pageshow

questions

12

Some languages let a class have two supertypes that share a common ancestor, and others refuse. Explain why inheriting a type, inheriting an implementation, and inheriting state are three separate problems, and show how at least three named languages draw the line differently.

level: middleimportance: must knowfreq 58%

answer

  1. type = obligations, unions freely
  2. implementation = conflict, but decidable
  3. state = share or duplicate, no right default
  4. C++ virtual base vs replicated subobject
  5. Java/C#/Rust: bodies yes, fields never

basics

~20 s

Types are only obligations, so they union freely. Implementation conflicts are decidable — pick one, order them, or error. State is the hard layer: a shared ancestor's fields must be duplicated per path or shared, and neither default is right for every program.

solid answer

~60 s

The three layers fail differently, and languages cut at different depths. - **C++** inherits all three. By default every path gets its **own copy** of the shared ancestor's subobject, so a diamond-shaped class holds two sets of those fields and an unqualified access is ambiguous; marking the base `virtual` collapses them into one, paid for with a runtime offset lookup and with construction of that base moved to the most-derived class. - **Python** also inherits all three, but an instance has a single attribute dictionary, so duplication is not even expressible — there is always exactly one `self.n`. The cost is that unrelated mixins silently share one name namespace. - **Java** allows many supertypes for type and, since Java 8, for implementation via default methods — but never instance fields. Two unrelated defaults are a compile error until the class overrides. - **Rust** traits carry bodies but no per-instance data; two traits may both supply `m`, and the *call site* disambiguates. State is what forces the share-or-duplicate choice, so languages that ban inherited fields dodge the diamond's hard half.

code

cpp · 12 lines
cpp
struct Base { int n = 0; };
struct L : Base {};
struct R : Base {};
struct D : L, R {};

D d;
// d.n = 1;          // error: ambiguous, there are two Base subobjects
d.L::n = 1;
d.R::n = 2;          // two independent fields in one object

// struct L : virtual Base {}; struct R : virtual Base {};
// -> exactly one Base, and D's own constructor initialises it

go deeper

for a junior

Recall the three layers by name and give one example each: a class can promise many types freely, two inherited method bodies can clash, and two inherited copies of the same fields is the genuinely hard case.

for a middle

Be able to draw the diamond and answer "one copy or two?" for C++ (two by default, one with a virtual base), Python (always one, single attribute dictionary), and Java (the question cannot arise, no inherited fields).

for a senior

Explain why no default is universally correct — independent bookkeeping mixins want two copies, identity fields want one — and what banning inherited fields actually buys: it converts an undecidable semantic question into an ordinary API design question.

for a principal

Frame it as a language-design tradeoff: where you cut determines whether reuse conflicts surface at declaration time, compile time, or call site, and argue for composition plus abstract accessors as the design that keeps the sharing decision explicit and reviewable in a large codebase.

## Three things a supertype can hand down When a class names a supertype it can acquire up to three different things, and answers that collapse them into "multiple inheritance is dangerous" miss where the real difficulty actually sits. 1. **Type** — the promise that instances can be used wherever the supertype is expected, plus the set of operation names and signatures that promise entails. Type is a *set of obligations*, and sets union without conflict: being both comparable and serializable demands two things and creates no ambiguity. Practically every language allows unlimited multiple inheritance of type. Even Java, popularly described as having "no multiple inheritance", lets a class implement any number of interfaces. 2. **Implementation** — actual method bodies. Two supertypes may supply a body for the same signature. Now there *is* an ambiguity, but it is decidable: pick one by rule, reject the program, or compute an order. Nothing about the object's memory layout is at stake, so any consistent answer works. 3. **State** — fields. This is where a genuine, unavoidable design choice appears, and it appears specifically in the *diamond*: class D reaches ancestor A through two different paths. Does a D object contain one A-worth of fields or two? ## Why the diamond of state has no default answer Both answers are sometimes correct. If A holds a `counter` and the two intermediate classes are two independent bookkeeping mixins, two copies is exactly what you want — each mixin counts its own thing. If A holds an `id` that identifies the object, two copies is nonsense; the object would have two identities. A compiler cannot infer which case you are in from the declaration alone. So a language must either pick a default and offer an opt-out, ban the diamond outright, or ban inherited fields altogether so the question never arises. ## How named languages land **C++** takes the maximal position and hands the choice to the programmer at *declaration* time. Ordinary (non-virtual) inheritance replicates: with `struct L : Base` and `struct R : Base`, a `struct D : L, R` object contains two `Base` subobjects, and an unqualified `d.n` is ambiguous — you must write `d.L::n` or `d.R::n`, and they are two independent fields. Declaring `struct L : virtual Base` and `struct R : virtual Base` makes the `Base` subobject **shared**, so all paths see one copy. The cost is real, and is why sharing is not the default: a non-virtual base sits at a compile-time-constant offset inside the object, whereas a virtual base's location must be found through an indirection at run time. Responsibility for constructing the shared base also moves to the most-derived class — the intermediate classes' initialisers for it are ignored, which surprises people the first time they hit it. **Python** allows multiple inheritance of everything, yet the replication question cannot arise, because instance state lives in one `__dict__` keyed by attribute name. There is always exactly one `self.n` no matter how many ancestral paths declare it. That is a genuinely different resolution, not a weaker one: Python did not solve the share-or-duplicate problem, it made duplication inexpressible. The price is paid elsewhere — two unrelated mixins that both use `self._cache` collide silently, with no diagnostic at class-creation time and no way to ask for separate storage short of name mangling or composition. **Java** and **C#** cut between implementation and state. A class may inherit any number of interfaces, and since Java 8 (C# 8 for default interface members) those interfaces can carry method bodies — but never instance fields. So implementation conflicts exist and are settled by rule: in Java, if one interface extends the other the more specific default wins; if they are unrelated, it is a compile error and the class must override, optionally delegating with `Loud.super.say()`. No layout question ever arises, because no field was inherited. **Rust** goes one step further. A trait carries method bodies, associated types and consts, but no per-instance data whatsoever. Two traits may each provide `m` for the same type, and that is not an error at all — it becomes an error only at a call site that cannot tell which is meant, and is resolved there with fully-qualified syntax. Storage a trait needs is requested as a required accessor method the implementor supplies, which turns the sharing question into an explicit part of the implementor's API rather than a layout decision. ## The transferable conclusion Banning inherited fields is the cheap way out of the diamond: it converts a semantic problem the compiler cannot solve (one copy or two?) into an API problem the programmer already knows how to solve (declare an accessor, or hold a component and delegate). That is why blanket condemnation of multiple inheritance is too coarse — the danger is concentrated in the state layer, and a language that permits multiple inheritance of bodies without fields has taken almost none of the risk.

  • Why does C++ require the `virtual` keyword on the base instead of simply making sharing the default?
    Because sharing is not always what you want, and it is not free. A non-virtual base sits at a compile-time-constant offset inside the derived object, so member access is a fixed address computation; a virtual base must be located through an indirection at run time. Sharing also changes construction semantics — the most-derived class becomes responsible for initialising the virtual base, and intermediate classes' initialisers for it are ignored. C++ defaults to the cheaper, more local behaviour and makes you opt in to sharing.
  • If a language bans inherited fields, how do you express "these two mixins both need a counter"?
    You make the storage part of the contract instead of part of the layout: the trait or interface declares an abstract accessor (say `count()` / `setCount()`) that the implementing class must provide. The implementor then decides explicitly whether the two mixins get one field or two, which is precisely the decision the compiler could not make for you. The alternative is composition — hold each mixin as a member object with its own state and forward the calls.
  • Java forbids inherited fields but interfaces can declare constants. Doesn't that reintroduce the diamond?
    No, because interface fields are implicitly `public static final` — they belong to the interface, not to any instance, so there is nothing to duplicate per object and no layout question. You can still get a *name* ambiguity if two inherited interfaces declare a constant with the same simple name, but that is resolved by qualifying it with the interface name, exactly like the default-method case.

Type is a job description, implementation is a trained employee, state is the desk they sit at. Two job descriptions merge fine, two employees can be told who leads, but two desks cannot occupy one square metre — somebody has to decide whether they share it.

saying these in an interview costs you the question

  • Saying "Java has no multiple inheritance" without qualifying it — a Java class may implement any number of interfaces, and since Java 8 those may carry bodies.
  • Treating the diamond problem as purely a method-resolution problem; the resolvable half is method bodies, the hard half is whether the shared ancestor's fields are duplicated or shared.
  • Claiming C++ virtual bases are free or are simply the better choice — they cost a runtime indirection and move construction of the base to the most-derived class.
  • Asserting Python "solves" the diamond with its method order; the resolution order settles which body runs, while the state question disappears only because instances have a single attribute dictionary.
  • Saying multiple inheritance of interfaces with default bodies is as risky as C++ multiple inheritance — without inherited fields, the layout risk is gone entirely.

context

open as a page

In several widely used languages the question 'should this be an abstract class or an interface?' cannot be asked in that form at all. Name such languages, say what replaces the choice there, and explain what actually drives the decision instead.

level: middleimportance: should knowfreq 40%

basics

~20 s

Go and Rust have no implementation inheritance, so there is no abstract class: you declare an interface or trait and reuse code by composition or default bodies. In C++ an interface is just an abstract class with only pure virtual functions. Python offers abc.ABC or typing.Protocol.

open as a page

Reusable bundles of methods come in two flavours: ones copied into the composing class so that a name clash is an error you must resolve, and ones inserted into the lookup chain so that composition order decides the winner. Contrast the two models and name languages of each kind.

level: middleimportance: should knowfreq 40%

basics

~20 s

Flattening (Pharo traits, PHP traits, Rust traits): methods are copied in, composition order is irrelevant, and a clash is reported and resolved by exclusion or aliasing. Chain insertion (Ruby modules, Scala traits): the unit becomes a link in the lookup chain, so order decides and forwarding continues along it.

open as a page

Not every abstract type can be used as a runtime-polymorphic reference. Rust rejects `dyn Trait` for a trait whose method is generic or returns `Self`; Swift could not use a protocol with `Self` or associated-type requirements as an existential (`any Protocol`) at all until SE-0309; C# 11's static abstract interface members are reachable only through a constrained type parameter, never through an interface-typed variable; Java has no such rule but also cannot declare `abstract static`. What single underlying rule explains all four, and how do you split an abstract type that breaks it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

An erased reference carries a value plus a fixed table of code addresses, but not the concrete type. Members needing the type — constructors, statics, constants, Self returns, generic methods — cannot cross. Rust, Swift and C# declare them and restrict use; Java forbids declaring them.

open as a page

Your library publishes both an abstract base class that outside teams subclass and an interface they implement. Set aside anything to do with default method bodies on the interface: what does shipping the base class commit you to that the interface does not, and how do the languages you know differ when you later add a concrete method or a data member to that base?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Publishing a base spends every subclass's one inheritance slot and turns its constructors and protected members into API. Later adding a concrete method can silently capture a subclass's own method — silent in Java, a hiding warning in C#, a compile error in Kotlin and Swift.

open as a page

A type acquires two interfaces that each supply a body for a method with the same name and signature. Compare where different languages report that conflict and what they force the programmer to write.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Java reports it at the class declaration and makes you override, disambiguating with Iface.super.m(). C# prefers the most specific implementation or errors, and the body is reachable only through an interface-typed reference. Rust defers to the call site and wants fully-qualified syntax. Swift extension members may resolve silently.

open as a page

In several languages a contract can constrain a type parameter at compile time yet cannot be used as the type of a runtime value that holds some unknown implementer: Rust rejects certain traits behind `dyn`, Swift distinguishes `any P` from `some P`, Go's method-set rule decides whether a value or only a pointer satisfies an interface, and C# forbids calling a `static abstract` interface member on an interface-typed variable. Which kinds of member make a contract unusable as a runtime value type, and how do you design a contract so it stays usable?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A contract can type a runtime value only if every member is callable on a receiver whose concrete type is unknown. Generic methods, Self-returning methods and receiver-less statics are not. Rust bars those traits from dyn, Swift splits any from some, Go keeps pointer-receiver methods out of a value's method set.

open as a page

Some languages model a callback as a one-method interface, while others have first-class function types. Compare the two encodings and explain what each one makes possible that the other does not.

level: seniorimportance: should knowfreq 45%

basics

~20 s

A single-abstract-method interface is a nominal function type: it has a name, documentation, extra default helpers and overload identity, but two identically shaped ones are incompatible. Real function types — Kotlin's (A)->B, Go's func types, Swift closures — are interchangeable by shape but anonymous.

open as a page

When two inherited paths supply a body for the same operation, some languages compute a total order over all ancestors and silently pick a winner, while others refuse to compile until the programmer names one. Compare the two resolution schemes and say what each one costs.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Computed orders (Python's C3, Scala's trait linearization) always produce an answer and let a call chain cooperatively through ancestors the author never named - at the cost of non-local reasoning. Explicit disambiguation (C++, Java, Rust) never surprises you but cannot express stacking and adds boilerplate.

open as a page

Can a reusable unit of behaviour own instance state of its own? Compare how several named languages answer, and say what goes wrong in each.

level: seniorimportance: should knowfreq 34%

basics

~20 s

Answers split three ways. Scala traits may declare fields, with a linearization-ordered initialization hazard. Ruby modules assign undeclared instance variables that silently collide. Rust, Java, C# and Swift forbid stored state and make the unit demand an accessor instead.

open as a page

An abstract class exposes shared state and protected hook methods to its subclasses. Explain why that creates a second API you must maintain, and compare how Kotlin's final-by-default classes, Swift's `open` versus `public`, C++'s non-virtual default and Python's convention-only access change what you can safely change later.

level: principalimportance: nice to knowfreq 27%

basics

~20 s

Every protected field, hook and self-call is a contract with subclasses you cannot see, and changing it breaks them like a public change would. Languages differ in the default: Kotlin classes and members are final unless open, Swift types are subclassable outside their module only if open, C++ methods are non-virtual unless virtual, Java and Python are open by default and Python enforces nothing.

open as a page

You need to express a property of a type that adds no operations — 'instances may be moved between threads', 'this may be persisted'. Compare expressing it as an empty interface, as metadata such as an annotation or attribute, and as a compile-time predicate.

level: principalimportance: nice to knowfreq 28%

basics

~20 s

An empty interface is checkable by the type system and usable as a generic bound, but only the type's author can declare it and every subtype inherits it whether it stays true or not. Annotations need reflection. Rust's Send and Sync show a third way: compiler-derived from the fields, with an unsafe opt-in.

open as a page