Abstraction is not encapsulation
Separate hiding how a thing stores its data from hiding which thing you hold, build each without the other, and spot an interface that leaks its cost.
Encapsulation hides how. Abstraction hides which. They travel together often enough that candidates fuse them into one word, and the fusion collapses the moment an interviewer asks for an example of one without the other.
Encapsulation is about a single class: the representation is private, so it can change without touching callers — the count argument in encapsulation. Abstraction is about a set of classes: the caller names an interface, so the concrete class behind it can be swapped without touching callers. Different hiding, different payoff, and each is available on its own.
Encapsulation with no abstraction
A money type with a private amount, arithmetic that maintains the unit, and exactly one implementation:
class Money:
__slots__ = ('_cents',)
def __init__(self, cents: int):
self._cents = cents
def plus(self, other: 'Money') -> 'Money':
return Money(self._cents + other._cents)
def __str__(self) -> str:
sign = '-' if self._cents < 0 else ''
c = abs(self._cents)
return f'{sign}{c // 100}.{c % 100:02d}'
public final class Money {
private final long cents;
private Money(long cents) { this.cents = cents; }
public static Money ofCents(long cents) { return new Money(cents); }
public Money plus(Money other) { return new Money(cents + other.cents); }
}
Callers never touch the field, so switching the storage to a scaled decimal is
one file. The constructor fixes the input unit; the storage stays private. That
is full encapsulation. There is no abstraction at all: there is no MoneyLike
interface, no second implementation, and every caller writes the concrete type
name Money. Nothing is hidden about which class they hold, because there is
only one.
This is the common and correct case. Most classes should look like this. An interface with a single implementation adds a name, a file and an indirection and hides nothing, because there is nothing to choose between.
Abstraction with no encapsulation
Now the reverse — an interface over types whose innards are wide open:
public interface Shape { double area(); }
public final class Rect implements Shape {
public double w, h; // no hiding whatsoever
public double area() { return w * h; }
}
public final class Circle implements Shape {
public double r;
public double area() { return Math.PI * r * r; }
}
from typing import Protocol
class Shape(Protocol):
def area(self) -> float: ...
class Rect:
def __init__(self, w, h):
self.w, self.h = w, h # public, by design
def area(self) -> float:
return self.w * self.h
A renderer that takes a Shape and calls area() is fully abstracted: it does
not know or care which class arrived, and a new Triangle costs the renderer
nothing. It is also completely unencapsulated: anyone holding a Rect can write
r.w = -1 and produce a negative area, and changing Rect to store two corner
points breaks every line that touched w or h.
Both halves of that are worth saying aloud, because they are independent. The
renderer's abstraction survives the representation change; the direct users of
Rect do not.
The tool, and what it costs
An interface is a named set of operations with no instance state: Java's
interface, Python's Protocol or an ABC, a C++ class with only pure virtual
functions. Since Java 8 a default method can carry shared code, so what
separates an abstract class is state, not code.
Reach for the interface first. A Java class has one superclass and any number of interfaces, so an abstract base spends the single inheritance slot; in C++ you can inherit several bases, but sharing state through them drags in virtual inheritance and its layout cost. Use an abstract class only when there is real shared state to hold, not to give the family a home.
The runtime cost of dispatching through an interface is small and often misquoted. The vtable load is a read from a line that is almost always hot — call it an L1 reference, about 1 ns, against a main-memory read at 100 ns. What actually costs is upstream: the compiler cannot inline through a call site that sees many types, so a two-line method stays a call instead of disappearing. That matters in an inner loop and nowhere else.
When the abstraction leaks
An abstraction leaks when the interface promises uniformity that the
implementations cannot deliver. The classic case is indexed access on a
sequence: get(i) reads the same in both implementations and costs wildly
different amounts.
Work the numbers. A loop that visits every element by index makes n calls. On an
array the element address is a base plus an offset, an L1 reference at about
1 ns, so n = 10⁴ costs 10⁴ × 1 ns = 10 µs. On a node chain, get(i) walks i
links from the head, and each link is a pointer chase to an address the
prefetcher cannot guess — a main-memory reference at about 100 ns. The total is
0 + 1 + 2 + … + (n−1) ≈ n²/2 = 5 × 10⁷ hops, and 5 × 10⁷ × 100 ns ≈ 5 s.
Ten microseconds against five seconds, from code that did not change. The pointer-chasing behind that 100 ns is the same one covered in pointer surgery.
The interface is not wrong; it is incomplete. It abstracted which structure without abstracting the cost, and cost is part of what a caller has to know. The repairs are all forms of putting the cost back in the type:
- Only expose operations both can do cheaply. Iteration is one step per
element for both, so an
Iterablecontract with aforloop is honest whereget(i)is not. - Split the interface. Java's answer was a marker,
RandomAccess, added so library code could branch on it — an admission that one interface had covered two cost classes. - State the cost in the contract. If
get(i)is documented as constant time, an implementation that walks a chain is not a valid one.
Encapsulation leaks this way less often, because the caller was not choosing the
representation — but it is not immune. Swapping Cart's private list for a dict
turns a linear scan into a hash lookup, and a private field that becomes a lazy
query buys a 0.5 ms round trip, both behind an unchanged signature. Abstraction
hides a choice the caller may still need to reason about, which is why it leaks
by default.
In an interview
The question is usually the flat one: "what is the difference between abstraction and encapsulation?" The weak answer defines both in terms of "hiding" and stops, which is where most candidates are.
Give the two-axis version and then an example on each axis. "Encapsulation hides
the representation of one class — I can change Money from cents to a decimal
without touching callers. Abstraction hides which implementation is behind an
interface — a renderer that takes Shape never names Circle. Money is
encapsulated with no abstraction. A Shape with public fields is abstracted with
no encapsulation." That answer cannot be produced by memorising a definition,
which is what makes it worth points.
If there is room, add the leak: an interface that hides which implementation you
hold also hides its cost model, and name get(i) on a linked list as the case.
The mistake that loses points: calling abstract classes "abstraction" and
private fields "encapsulation", as if the keywords were the concepts. Both are
achievable with neither keyword, and Python does exactly that — Protocol for
abstraction with no inheritance, an underscore for encapsulation with no
enforcement.
Check yourself
A class has all-private fields, full accessor discipline, and no interface and no subclasses. Which of the two does it have?
Encapsulation only, and possibly not even that — private fields with a getter per field still publish the representation. There is nothing to abstract over with one implementation, and adding an interface for it would hide nothing.
A plugin interface has one method, run(config). Two implementations exist:
one returns in about 1 ms, the other calls a service in another region, India
to US East, and returns in about 200 ms. What has leaked, and what would you
change?
The interface hides which implementation you hold, so a caller cannot tell a local call from a 200 ms round trip and will write it into a synchronous loop. Cost is part of the contract: make the slow one return a future or take a timeout, so the signature carries the fact that it can block.
You are told to add an interface for every class in the codebase. Give the one-sentence objection.
An interface with one implementation hides no choice, so it buys no substitutability while adding a file, a name and an indirection the reader has to follow — abstraction is only paid for when something is actually being chosen between.