Oop

Encapsulation, Inheritance & Polymorphism

Three object-oriented ideas as design tools. Encapsulation keeps a class's rules in one place, so no caller can break them; access modifiers set how much of a class is its public contract. Inheritance models is-a and reuses code, but inheriting methods that break your rules (java.util.Stack) is worse than composing. Polymorphism replaces type checks with one call that each class answers its own way. The Java language rules live in the Java guide; every example here runs in Java and Python, with verified output.

Suggest an edit

Encapsulation, Inheritance & Polymorphism — OOP as a Design Tool

Every OOP course lists the same four pillars: encapsulation, inheritance, polymorphism and abstraction. Learning their syntax is the easy part. The design question is when each one helps, and when it quietly makes a system harder to change.

This lesson looks at the first three as design tools. The fourth, abstraction, is covered in the next lesson. The Java language rules (private, extends, super, overriding, overloading) are in the Java guide's Encapsulation & Access Modifiers and Inheritance & Polymorphism. This lesson links to those pages instead of repeating them.

💡 The core idea.

  • Encapsulation keeps a class's rules in the same place as its data, so every change to the data goes through code that checks the rules.
  • Inheritance expresses "is a" and shares code, but a subclass inherits every public method of its parent. If some of those methods would break the subclass's rules, use composition instead: hold the other object in a field.
  • Polymorphism lets one method call do the right thing for each type of object, so supporting a new type means adding a class, not editing every switch statement.

Every output below was produced by running the code on Java 21 and Python 3.11.

You'll be able to: explain what encapsulation protects, and choose the smallest visibility for each member; spot inheritance that exposes methods the subclass should not have, and replace it with composition; replace a switch on a type code with polymorphism, and explain what that gains when a new type is added.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the design does to the code that uses it.
  2. A concrete bite — a specific, runnable program where the design choice produces a bug or prevents one.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Encapsulation: keep the rules next to the data
  2. Inheritance: is-a, and what you inherit
  3. Polymorphism: one call, many behaviours
  4. Mental-model summary
  5. Gotcha checklist
  6. Check yourself
  7. Sources

1. Encapsulation: keep the rules next to the data

A bank account has one rule: the balance never goes below zero. Here the balance is a public field:

Output:

balance = -150

Analysis. Nothing stopped the overdraft. The rule "no negative balance" exists only in the memory of each programmer who writes code that uses the account, and every one of them must remember it. With a hundred places in the code that change the balance, one will forget.

Encapsulation makes the field private, and offers methods that enforce the rule:

Output (Java; Python prints False and True):

withdraw 250: false
withdraw 40:  true
balance = 60

Analysis. The overdraft was refused, and the valid withdrawal went through. The rule now lives in exactly one place, withdraw, and no other code can get around it: the Java compiler rejects any access to balance from outside the class. Python has no private keyword. Instead, a leading underscore is a convention that means "internal, don't touch", and a read-only @property lets callers read the value without being able to set it.

Intuition. Mechanism. A class's invariants are the rules that must be true for every object of that class at all times: a balance is never negative, an order's total equals the sum of its lines. With encapsulation, the class is the only code that can change its own state, so it is the only code that has to enforce those rules.

Concrete bite. Adding a getter and a setter for every field (getBalance(), setBalance()) is not encapsulation. setBalance(-150) breaks the rule just as the public field did. Offer methods that mean something in the business (deposit, withdraw), not setters for raw fields.

Access modifiers decide which parts of a class other code may use, and so which parts are the class's promise to the outside world:

Java modifier Visible to Use it for
private the class itself all fields; helper methods
(none), package-private classes in the same package collaborators that work closely together
protected the package, plus subclasses hooks meant for subclasses
public everyone the contract: what callers may rely on

Everything public is a promise: changing it can break code you don't know about. The Java guide's Encapsulation & Access Modifiers covers the language rules [1].

💡 Earned rule. Make every field private. Give the class methods named after what happens in the business, and let each one check the rules. Make each method only as visible as the code that calls it needs.

The cost is writing methods instead of exposing data directly. The benefit is that each rule is enforced in one place, and you can change how the data is stored (whole cents instead of a double, say) without changing any code that uses the class.

Your Turn — Practice: Encapsulation

Model a library Book catalogue that hides its availability flags behind borrow / return / query methods — encapsulation in a realistic shape.


2. Inheritance: is-a, and what you inherit

Inheritance lets a subclass reuse its parent's code, and lets it be used anywhere the parent is expected. It models an is-a relationship: a Manager is an Employee. The catch is that a subclass inherits all of its parent's public methods, whether or not they make sense for it.

Java's own java.util.Stack class shows the problem. It extends Vector, which is a list:

Output (Java; Python prints the same with undo[0] and quotes around the items):

pop: type 'c'
get(0): delete line   <- a stack should not allow this
whole stack: [delete line, type 'a', type 'b']

Analysis. A stack should only let you add and remove items at the top. Because Stack is a Vector, it also has add(index, …), get(index) and every other list method. The "delete line" action was inserted at the bottom, so the undo history is no longer in the order things actually happened. The Stack documentation itself recommends Deque instead [2].

Composition gives a class only the methods it should have. Here the stack has a deque as a private field, and offers only push and pop:

Output:

undo type 'b'
undo type 'a'

Intuition. Mechanism. Inheritance ties the subclass to its parent's entire public interface, and to how the parent is implemented. Composition ties a class only to the methods it chooses to call. "Favor object composition over class inheritance" has been standard design advice since 1994 [3], and Bloch makes the same case with Java examples [4].

Concrete bite. The test for good inheritance is substitution: everything that is true of the parent must stay true of the subclass. This is the Liskov Substitution Principle from the SOLID chapter. Any code that receives a Stack as a Vector can treat it as a list, and so can break its stack behaviour, as the example showed.

💡 Earned rule. Use inheritance only when the subclass truly is a kind of its parent, everywhere the parent is used, and needs all of the parent's public methods. Otherwise use composition: hold the other object in a private field, and offer only the methods you intend to. Relationships and Object Behaviour covers composition in depth.

The cost of composition is writing small methods that pass calls on to the inner object. The cost of the wrong inheritance is a set of public methods you can never take back.

Your Turn — Practice: Inheritance

Build a small hierarchy — a base Employee, with Manager and Engineer deriving from it — where each subclass overrides displayDetails() but reuses the parent's version via super.


3. Polymorphism: one call, many behaviours

A ride app prices each vehicle type differently. The first version chooses the pricing rule with a switch on a type code, a string that names the vehicle type:

Output (Java; Python prints whole numbers):

BIKE, 10 km: 60.0
CAR, 10 km:  150.0
AUTO, 10 km: 0.0   <- added to the app, forgotten here

Analysis. When the AUTO type was added to the app, this switch was forgotten, so auto-rickshaw rides became free. Every place in the code that switches on the vehicle type (fares, icons, seat counts, insurance) must be found and edited for each new type.

Polymorphism moves each rule into the class for its type. The caller makes one method call, and each object responds in its own way:

Output (Java; Python prints whole numbers):

BIKE, 10 km: 60.0
CAR, 10 km: 150.0
AUTO, 10 km: 100.0

Analysis. v.fare(10) ran a different method for each vehicle. Adding Auto meant adding one class, and no existing code changed. Forgetting to write fare() no longer gives a silent 0 either: Java won't compile an Auto class without it, and Python won't create an Auto object without it.

Intuition. Mechanism. With runtime polymorphism, the method that runs is chosen by the class of the actual object, not by the type the variable was declared with [5]. Overloading, where several methods share a name but take different parameters, is resolved by the compiler. It is a naming convenience, not a design tool. The Java guide's Inheritance & Polymorphism covers both.

Concrete bite. This is the Open/Closed Principle from the SOLID chapter: the code is open to new vehicle types, but the existing pricing code doesn't need to change. It is also a small example of the Strategy pattern.

💡 Earned rule. When the same switch or if chain on a type code appears in more than one place, move each branch into its own class, behind a shared interface.

The cost is more classes, and one more step to follow when reading the code. For a single switch over a fixed set of types that will never grow, a switch is simpler. In Java, a sealed interface combined with a switch over its types makes the compiler check that every type is handled.


4. Mental-model summary

Principle Consequence
Encapsulation: private state, and methods that check the rules Each rule is enforced in one place, and no other code can break it
A setter for every field is not encapsulation Offer business methods (withdraw), not raw setters
Everything public is a promise Make each member only as visible as the code that uses it needs
A subclass inherits every public method It can end up offering methods that break its own rules (Stack.add(0, …))
Composition offers only the methods you choose Prefer it, unless the subclass truly is a kind of its parent everywhere
Runtime polymorphism picks the method by the object's class One call, many behaviours; a new type is just a new class
The same type-code switch in many places Every new type means finding and editing each one

5. Gotcha checklist

Symptom Likely cause Fix
Objects end up in impossible states (negative stock, empty names) public fields or raw setters private fields; operations that check the rules
A rule is checked in some callers but not others the rule lives outside the class move it into the class's operations
A subclass has methods that make no sense for it inheritance used only for code reuse composition: hold the other object, forward what you need
Code that takes the parent type breaks with the subclass the subclass is not substitutable (LSP) change the hierarchy, or compose
A new type silently gets default behaviour a type-code switch was missed polymorphism, or a sealed type with an exhaustive switch
The same switch appears in five classes behaviour keyed on a type code one method per behaviour, on each type

✅ Check yourself

One check per objective. Answer before you open anything.

An Order has a list of lines and a total field that must always equal the sum of the lines. How would you encapsulate it?

Make both fields private. Offer addLine(...) and removeLine(...) methods that update total in the same step. Or drop the field and compute total() from the lines whenever it is asked for, so there is no second copy of the information to keep in sync. Don't hand out the list itself; return a read-only view or a copy, or other code could add lines without updating the total.


📚 Sources

  1. The Java Language Specification, Java SE 21, §6.6 "Access Control" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-6.html#jls-6.6
  2. java.util.Stack, Java SE 21 API ("A more complete and consistent set of LIFO stack operations is provided by the Deque interface") — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Stack.html
  3. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994), ch. 1.
  4. Joshua Bloch, Effective Java, 3rd ed. (Addison-Wesley, 2018), Item 18, "Favor composition over inheritance".
  5. The Java Language Specification, Java SE 21, §15.12.4.4 "Locate Method to Invoke" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.12.4.4

🧪 Predict, then check.

  1. In the encapsulated account from section 1, call withdraw(-50). Predict the result and the balance.
  2. In the composed UndoStack from section 2, uncomment the add(0, …) line. Predict what happens in Java and in Python.
  3. In section 3, add a Truck with fare = 50 + 15 * km to the polymorphic version. Predict the line it prints, and which other classes you had to change.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionff97b7f