The Four Pillars of OOP with Examples
Four ideas, and each exists to remove a specific kind of pain from a growing codebase. Learn them as answers to problems rather than as vocabulary, and the interview follow-ups stop being hard.
1. Encapsulation — hide the state
Keep data private and expose behaviour instead. The point is not secrecy; it is that invariants can be enforced in one place. If the balance field is public, any line in the codebase can make it negative. If it is private, only the class can change it — and the class can refuse.
public class Account {
private long balancePaise; // nobody outside can touch it
public void withdraw(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
if (amount > balancePaise) throw new InsufficientFundsException();
balancePaise -= amount; // the only path that can change it
}
public long balancePaise() { return balancePaise; }
}
A getter and setter for every field is not encapsulation
getBalance() plus setBalance() is a public field with extra typing — the invariant is still unprotected. Encapsulation means exposing operations (withdraw, deposit), not fields.
2. Abstraction — hide the mechanism
Encapsulation hides data; abstraction hides how. Callers depend on a contract — an interface — and stay unaffected when the implementation behind it changes. That is what makes a payment provider swappable and a class testable with a fake.
interface PaymentGateway {
PaymentResult charge(long amountPaise, Card card);
}
class RazorpayGateway implements PaymentGateway { /* HTTP calls */ }
class FakeGateway implements PaymentGateway { /* used in tests */ }
class Checkout {
private final PaymentGateway gateway; // the contract, not the class
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
}
| Encapsulation | Abstraction | |
|---|---|---|
| Hides | State | Implementation |
| Achieved with | private fields, public methods | interfaces, abstract classes |
| Answers | Who may change this data? | What does this thing do? |
| Protects against | Broken invariants | Ripple effects from change |
3. Inheritance — reuse a type
A subclass gets the parent's fields and methods, and can add or override. The valuable part is not code reuse — it is that the subclass is the parent type, so anything accepting the parent accepts it.
The test is the Liskov Substitution Principle: any place the parent works, the child must work too. The textbook violation is Square extends Rectangle — code that sets width and height independently and checks the area breaks the moment a Square is passed in, because a square cannot honour that behaviour.
4. Polymorphism — one call, many behaviours
The same call dispatches to different implementations depending on the actual object. It is what lets you delete a chain of ifs and add a new case by adding a class.
abstract class Notification {
abstract void send(String to, String body);
}
class EmailNotification extends Notification { void send(String t, String b) { /* SMTP */ } }
class SmsNotification extends Notification { void send(String t, String b) { /* gateway */ } }
class PushNotification extends Notification { void send(String t, String b) { /* FCM */ } }
// The caller never branches on type - a new channel needs no change here
void notifyAll(List<Notification> channels, String to, String body) {
for (Notification n : channels) n.send(to, body);
}
| Overloading | Overriding | |
|---|---|---|
| Also called | Compile-time / static polymorphism | Runtime / dynamic polymorphism |
| Same method name with | Different parameter lists | The same signature in a subclass |
| Resolved | By the compiler, from the declared types | At runtime, from the actual object |
| Return type may differ | Yes | Only covariantly |
Calling an overridable method from a constructor
The subclass constructor has not run yet, so the override executes against uninitialised fields. It compiles, it looks harmless, and it produces nulls that are painful to trace. Constructors should call only private or final methods.
Key takeaways
- Encapsulation hides state so invariants are enforced in exactly one place.
- Abstraction hides implementation so callers survive change.
- Inheritance creates a subtype, and the subtype must be substitutable for its parent.
- Polymorphism replaces branching on type with dispatch, so new cases mean new classes.
Frequently asked questions
What is the difference between abstraction and encapsulation?
Encapsulation hides data behind methods so invalid states cannot be created from outside. Abstraction hides how something is implemented behind a contract, so the implementation can change without affecting callers.
What is the difference between method overloading and overriding?
Overloading is several methods with the same name but different parameter lists, resolved by the compiler. Overriding is a subclass replacing a parent method with the same signature, resolved at runtime from the actual object type.
Can you achieve abstraction without an interface?
Yes. An abstract class provides abstraction plus shared state and implementation. An interface is preferred when you only need the contract, because a class can implement many interfaces but extend only one class.
Test yourself on OOPs Concepts
Reading is not recall. Take a timed quiz on this topic solo, or share a room code and battle friends on it.
⚡ Start the OOPs Concepts quiz