On this page
Description
Object-oriented programming is a set of tools for putting data and the rules about that data together, and for letting different kinds of objects be used through one common shape. This skill covers its four classic ideas - encapsulation, abstraction, inheritance and polymorphism - and the most important design habit built on them: preferring composition to inheritance.
Here's the kind of code these ideas fix. Imagine an online shop written without objects: a map of fields for each product, a pile of static helper functions, and if type == "BOOK" checks wherever books behave differently from electronics. It works at first, but every rule lives far from the data it protects, anyone can put an invalid price into the map, and adding a product type means hunting down every if.
We'll go from simple to more technical:
- The four pillars, each in one sentence, in Java and Python.
- Composition over inheritance, and the classic example of getting it wrong.
- How to decide between them, step by step.
- Where you'll see these choices in real libraries and frameworks.
The four pillars
| Pillar | In one sentence | Java tools |
|---|---|---|
| Encapsulation | Keep an object's data private and change it only through methods that protect its rules. | private fields, methods that validate |
| Abstraction | Expose what an object does, hide how it does it. | interfaces, well-named public methods |
| Inheritance | Build a new class on top of an existing one ("is-a"). | extends |
| Polymorphism | One call, many behaviors: the object decides what the call means. | overriding, interfaces |
Python uses different tools for the same pillars: encapsulation is a convention (a leading _ marks an attribute as internal, and @property gives read-only access), abstraction uses abstract base classes (abc.ABC) or typing.Protocol, inheritance is class Savings(Account), and polymorphism needs no declared interface at all - any object with the right method works (duck typing).
Java
interface Shape { double area(); } // abstraction
final class Circle implements Shape {
private final double radius; // encapsulation: private, validated once
Circle(double radius) {
if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
this.radius = radius;
}
public double area() { return Math.PI * radius * radius; } // polymorphism
}
double total = shapes.stream().mapToDouble(Shape::area).sum(); // no type checks
Python
import math
from abc import ABC, abstractmethod
class Shape(ABC): # abstraction
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, radius: float):
if radius <= 0:
raise ValueError("radius must be positive")
self._radius = radius # encapsulation: internal by convention, validated once
def area(self) -> float: # polymorphism
return math.pi * self._radius ** 2
total = sum(s.area() for s in shapes) # no type checks
Composition over inheritance
Inheritance is the pillar most often misused. extends gives you every public method of the parent - whether it makes sense or not - and ties you to its internals.
Java
// java.util.Stack extends Vector: a "stack" that also lets anyone insert into the middle.
Stack<Integer> stack = new Stack<>();
stack.push(1); stack.push(2);
stack.add(0, 99); // legal, because Stack IS-A Vector - and LIFO is broken
Python
class Stack(list): # a "stack" that IS-A list...
def push(self, x) -> None:
self.append(x)
stack = Stack()
stack.push(1)
stack.push(2)
stack.insert(0, 99) # legal, because Stack IS-A list - and LIFO is broken
Composition means having an object instead of being one: a class holds a field and exposes only the operations it wants.
Java
final class BoundedStack<T> {
private final Deque<T> items = new ArrayDeque<>(); // has-a, not is-a
private final int capacity;
BoundedStack(int capacity) { this.capacity = capacity; }
void push(T item) {
if (items.size() == capacity) throw new IllegalStateException("stack is full");
items.push(item);
}
T pop() { return items.pop(); }
}
Python
class BoundedStack:
def __init__(self, capacity: int):
self._items = [] # has-a, not is-a
self._capacity = capacity
def push(self, item) -> None:
if len(self._items) == self._capacity:
raise RuntimeError("stack is full")
self._items.append(item)
def pop(self):
return self._items.pop()
How to decide, step by step
- Ask "is-a or has-a?" A
Carhas anEngine; aSavingsAccountis anAccount. - Even for is-a, check substitutability: can the child be used everywhere the parent is, without surprises? (If not, inheritance will hurt - see the Liskov Substitution Principle.)
- If you only want to reuse code, prefer composition: hold the helper as a field and delegate to it.
- If you want many types to be used interchangeably, prefer an interface plus composition over a deep class hierarchy.
Where you'll see it
- The JDK's own regrets:
Stack extends VectorandProperties extends Hashtableare classic examples of inheritance exposing the wrong API. Python has its own version: subclassing the built-inlistordictis full of surprises, because their C methods don't call your overrides (extendnever calls yourappend) -collections.UserListandUserDictexist for exactly that reason. - Modern frameworks favor composition: Spring beans receive collaborators through their constructors instead of inheriting behavior.
- Game engines moved from deep
Enemy -> FlyingEnemy -> FlyingShootingEnemyhierarchies to entity-component systems - composition at scale.
Which tool for which job?
| You want to | Use |
|---|---|
| Protect a rule about an object's data | private fields (Java) or _name attributes (Python), changed only by methods that check the rule |
| Let callers use many kinds of objects the same way | an interface (Java) or an ABC / Protocol (Python) |
| Reuse another class's code | composition: hold it as a field and delegate |
| Model a genuine "is-a" with a base designed for extension | inheritance |
| A closed, known set of types | sealed interfaces (Java 17+); typing.final and checks in Python |
| Stop anyone subclassing a class | final (Java); @typing.final, checked by type checkers (Python) |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Extending a class just to reuse a few methods | you also expose every other method it has | compose: hold it as a field |
Extending a library collection (Vector, list, dict) to add rules | callers bypass your rules through inherited methods | wrap the collection |
| A getter and setter for every field | public fields with extra steps - no rule is protected | meaningful operations (deposit, withdraw) |
Deep hierarchies (Enemy -> FlyingEnemy -> FlyingShootingEnemy) | five files to understand one object | small interfaces plus composed parts |
if type == ... checks spread through the code | every new type means editing every if | polymorphism: each type implements the behavior |
Read more - design for inheritance, or prohibit it
A useful rule from Effective Java: design and document for inheritance, or else prohibit it. Making classes final by default (or using records and sealed interfaces) keeps you honest - inheritance becomes a deliberate, reviewed decision instead of an accident. Python has no enforced final: @typing.final is checked only by type checkers.
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Is getters-and-setters-for-every-field encapsulation? Why or why not?
Show answer
No. If every field can be read and written freely, the object can't protect any rule about its data. Encapsulation means the object's methods keep its invariants true - often there's no setter at all, just meaningful operations like deposit().
2. Why does java.util.Stack break the idea of a stack?
Show answer
Because it extends Vector, it inherits add(index, element), remove(index) and friends, so anyone can modify the middle of the "stack" - inheritance exposed an API that contradicts what the class is supposed to be.
3. You want ReportService to reuse the formatting code in PdfFormatter. Inherit or compose?
Show answer
Compose: ReportService has a PdfFormatter field and calls it. A report service is not a kind of formatter, and composition lets you swap in an HtmlFormatter later.
Key takeaways
- Encapsulation protects rules; abstraction hides details; inheritance models is-a; polymorphism lets one call mean different things.
- Ask "is-a or has-a?" - and even for is-a, check the child can stand in for the parent everywhere.
- Prefer composition for code reuse: hold a field, expose only what you mean to.
- Make classes
finalunless they're designed for inheritance. - Composition costs a few forwarding lines and buys control of your API and freedom to swap parts.