The 4 Pillars of OOP: Encapsulation, Inheritance, Polymorphism, Abstraction
Four words every developer has heard, occasionally confused, and sometimes been asked to define on the spot.
Encapsulation, inheritance, polymorphism, and abstraction.
In short:
- encapsulation hides state behind methods,
- inheritance reuses behavior across an “is-a” hierarchy,
- polymorphism dispatches one call to many implementations, and;
- abstraction hides implementation behind a contract.
The rest of this post is one Java example per pillar.
1. Encapsulation
Encapsulation means bundling state and the behavior that operates on it into a single unit, then hiding the state behind that behavior. Callers go through methods; they do not poke at fields. The point is control, not secrecy. The class owns its invariants, and outside code can only change the state through paths the class has signed off on.
public class Circle {
private final double radius; // <- the state we're hiding
public Circle(double radius) {
if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
this.radius = radius;
}
public double area() {
return Math.PI * radius * radius;
}
}radius is private and final: nothing outside the class can set it to a non-positive value, and nothing inside can reassign it after construction. The only way to learn anything about a Circle’s shape is to call area(). That is the whole game: invariants live next to the data, and the data is unreachable except through the methods the class provides.
You might be thinking: that’s it? Yep. Encapsulation is all about hiding (encapsulating) state.
2. Inheritance
Inheritance lets one class derive from another, picking up its fields and methods and optionally overriding or extending them.
In Java,
extendsis the keyword for inheritance.
It models an “is-a” relationship: a Circle is a Shape, so it provides its own area() and adds circle-specific behavior on top.
public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) {
if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
public double circumference() {
return 2 * Math.PI * radius;
}
}Circle does both things inheritance offers. The area() override fulfills the parent’s contract — every Shape declares an area(), and Circle provides the formula for circles. That is the point of overriding: give the parent’s method a body that makes sense for this subclass.
circumference() is the other half. Pure extension, no override, just a new method that only Circle needs.
The usual caveat: inheritance binds the child to the parent’s API forever, so reach for it only when the “is-a” relationship is genuine.
3. Polymorphism
Polymorphism is the ability to call one method on a parent type and have the runtime dispatch it to whichever subtype the object actually is.
Polymorphism means “many forms”.
The caller does not know or care which concrete class it is holding; it just calls the method, and the right implementation runs.
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("must be positive");
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
// A "shape" list can hold any shape
List<Shape> shapes = List.of(new Circle(5), new Rectangle(3, 4));
for (Shape s : shapes) {
System.out.println(s.area());
}Same call site, two behaviors. The loop has no if chain on shape type and it never will: when you add a Triangle next quarter with its own area(), this code keeps working untouched. That decoupling is what makes polymorphism worth the trouble of the type hierarchy.
4. Abstraction
Abstraction is exposing what something does and hiding how it does it, usually through an interface or an abstract class. It is often confused with encapsulation, but the distinction is clean:
- encapsulation hides state inside a class,
- abstraction hides implementation details (complexity) behind a contract.
Abstraction is about hiding complexity.
One protects data, the other protects callers from caring which concrete type they got.
public static double totalArea(List<Shape> shapes) {
double total = 0;
for (Shape s : shapes) {
total += s.area();
}
return total;
}totalArea knows nothing about circles or rectangles. It depends only on the contract: anything that is a Shape has an area(). Add a Triangle next month and totalArea does not change. That is the lever abstraction gives you. The other three pillars are quietly setting it up — state safe inside classes, hierarchies expressing relationships, dispatch routing calls. The result is calling code that talks to ideas instead of implementations.