The 5 Pillars of OOP — Classes, Encapsulation, Inheritance, Polymorphism, and Interfaces
The 5 Pillars of OOP — Classes, Encapsulation, Inheritance, Polymorphism, and Interfaces
🎯 What you'll be able to do after this lesson
After completing this lesson, you'll be able to confidently handle the following three things.
- ▸✅ Define encapsulation, inheritance, polymorphism, and abstraction — with code examples
- ▸✅ Answer interview questions on all 5 SOLID principles (especially SRP, OCP, and DIP)
- ▸✅ Explain in one sentence why composition beats inheritance
Treat your learning goals as a checklist — close this lesson only once you can answer all of them.
What is object-oriented programming — writing code *like the real world works*
The Core Idea
Object-oriented programming (OOP) = a programming paradigm that models real-world things as objects and has those objects exchange messages with one another to get things done. It is the central philosophy of Java.
Why OOP?
In the old C world, functions and data lived separately. A person's name and age were variables; walking and talking were functions — kept apart, so as a program grew larger it became impossible to tell what was what.
OOP bundles data and behavior together. Inside a "Person" class you have fields like name and age alongside methods like walk and talk — just as a real person has a body (data) and actions at the same time.
Class vs. Object — the mold and the fish-shaped cake
These two terms cause the most confusion. Here's an analogy.
- ▸Class = the mold. A blueprint. The plan that says "I'll make something shaped like this."
- ▸Object = the actual cake that comes out. You can make 1, 2, or 100 of them, and each one is independent.
The new keyword is the act of baking a cake. It carves out a new space in memory and records that location in the variable a.
Encapsulation — keeping outsiders from touching what they shouldn't
What if anyone could change your bank balance at will? That would be disastrous. So in OOP, direct external access is blocked, and data can only be handled through designated methods.
public int checkBalance() { return balance; }
}
The private keyword blocks direct access, and only official channels like deposit() are left open. Inside those channels you can handle all the extra work — validation, logging, synchronization. That's the essence of encapsulation.
Inheritance — reusing common parts
Suppose you're building class hierarchies for various animals. Dogs, cats, birds — they all move and eat. Writing that separately every time is duplication.
javaAnimal a = new Dog();
Animal b = new Cat();
a.sound(); // "Woof woof"
b.sound(); // "Meow"
``
Both a and b are of type Animal, but the sound() method behaves differently depending on the actual runtime type. It's decided at runtime.
Thanks to this, you can mix dogs and cats in a single List and process them all with one loop, each doing its own thing.
Interface vs. Abstract Class — a commonly confused pair
Both are abstract types that cannot be instantiated directly. The difference:
- ▸Interface — a role. A capability spec like "can be compared (Comparable)" or "can be iterated (Iterable)". Multiple implementation is possible (implements A, B, C
). - ▸Abstract class — a common skeleton. "These methods are already implemented, but some must be filled in by subclasses." Single inheritance only (extends A
).
Rule of thumb: use an interface when you want to define only a role, and an abstract class when you need shared code + some abstract methods. Since Java 8 interfaces can have default method implementations, the line has blurred — but the role vs. skeleton intuition still holds.
Quick Recap
OOP is about breaking a complex program into small units (classes) to manage it. The 5 core principles:
> 💡 In the real world: Java back-end development is 95% OOP. Functional and reactive styles are supporting tools. OOP is your primary weapon.
Exception Handling — dealing with errors *gracefully*
What is an exception?
When a program hits an unexpected situation — a file doesn't exist, a DB connection drops, a division by zero — Java throws an Exception object. If nobody catches it, the program crashes.
Unlike old C where you'd return an error code, this mechanism propagates errors on a separate path.
Two kinds of exceptions — Checked vs. Unchecked
This is Java's quirky feature — a distinction you almost never see in other languages.
- ▸Checked: the compiler forces you to handle it.
IOException,SQLException, etc. A try-catch orthrowsdeclaration is mandatory. - ▸Unchecked (RuntimeException): handling is recommended but not enforced.
NullPointerException,IllegalArgumentException, etc.
This distinction has long been controversial. Spring and modern libraries barely use Checked exceptions — mandatory handling is seen as polluting the code. Kotlin and C# have only Unchecked exceptions.
try-catch-finally
finally used to be the go-to for resource cleanup, but try-with-resources (Java 7+) is cleaner:
Any resource implementing AutoCloseable is automatically closed when the block ends. DB connections, files, and network sockets all follow this pattern as standard.
4 common pitfalls
1. catch (Exception e) { } — empty catch: the scariest anti-pattern. It silently swallows errors. The beginning of debugging hell. At the very least, leave a log.
2. e.printStackTrace(): stack traces get scattered to standard output. In production, use a logger:
3. Two different things in one try block: if you bundle different kinds of work inside one try block, you won't know where the error came from. Multiple small try blocks is better.
4. Exceptions without domain meaning: throw new RuntimeException("error") versus throw new InsufficientBalanceException() — a meaningful name makes a huge difference for debugging and documentation.
Quick Recap
Exception handling isn't about plastering your code with defensive checks. The core is to clearly separate normal flow from exceptional flow, and to enable future tracing with meaningful messages and logging.
Generics — treating *types like variables*
Why do we need generics?
Before Java 5, a List could hold any type. That seemed convenient, but it led to frequent ClassCastException explosions at runtime.
Generics is a mechanism that validates types at compile time.
Runtime explosions → caught in advance by the compiler. Type safety is the core value.
Basic Usage
<T> is a type parameter — similar in concept to a method parameter. The compiler infers the type at the call site.
Bounded Types — conditions on T
<T extends Number> means T can only be Number or one of its descendants. Only numeric types like Integer, Long, and Double are allowed through.
Wildcards — what ? means
This is the most confusing part.
- ▸
List<? extends Number>— some subtype of Number (unknown exactly). Read-only; no writing. - ▸
List<? super Integer>— some supertype of Integer (unknown exactly). Writing is possible (Integer only); reading yields only Object.
The PECS principle — Producer Extends, Consumer Super:
- ▸Where data is read out (producer) →
extends - ▸Where data is only put in (consumer) →
super
> No need to memorize. You only need to go deep on this when building a generic library. As a user, List<User> and similar straightforward usages cover 99% of cases.
Type Erasure — gone at runtime
A curious property of Java generics. After compilation, all <T> is erased and replaced with Object. As a result:
- ▸
new T()is not allowed (T is unknown at runtime) → you must receive aClass<T>argument instead - ▸Creating a
T[]array is not allowed - ▸
instanceof List<String>is not allowed → onlyinstanceof List<?>works
This is a design debt Java made once and can't undo. It would have been better as C#'s Reified Generics, but it can't be changed for compatibility reasons.
Quick Recap
Generics are a tool for compile-time type safety. Basic usage like List<User> is a must. Wildcards and bounds are something you go deep on when writing libraries.
enum + Annotations — Java's *meta tools*
enum — not just constants
In other languages, an enum is a simple collection of constants. Java's enum is far more powerful — each value is an object and can have methods, fields, and interface implementations.
Each enum value can carry extra data and you can define methods on it too. It's frequently used for state machines and the Strategy pattern.
Instead of a cascade of if-else, each enum value owns its own behavior.
Annotations — attaching metadata to code
@Override, @Deprecated, @SuppressWarnings — everything starting with @ is an annotation. It's not the code itself but information about the code.
Annotations are the heart of Spring
If you're doing Java back-end development, annotations will make up 80% of your code. Nearly all of Spring's magic is annotation-based:
- ▸
@Component,@Service,@Repository— bean registration - ▸
@Autowired— dependency injection - ▸
@RestController+@GetMapping("/users")— web routing - ▸
@Transactional— automatic transaction management - ▸
@Entity+@Id— JPA mapping
Annotations do nothing by themselves. A framework like Spring reads annotations at runtime and automatically performs the corresponding behavior (routing, DI, transactions).
Quick Recap
- ▸enum = type-safe constants + each value can have object-like methods and fields
- ▸Annotation = metadata. Not executed directly. Read and acted upon by frameworks and tools
These two are what make Java's declarative style possible. Instead of commanding "do it this way", you just declare "this is what it is" and the tools handle the rest.
The 5 SOLID Principles — bad vs. good code, side by side
What is SOLID?
An acronym for 5 object-oriented design principles, compiled by Robert C. Martin (Uncle Bob). You will be asked about these in every interview.
S — Single Responsibility
One class does one thing. It should have only one reason to change.
O — Open/Closed
Open for extension, closed for modification. Adding new features should be possible without changing existing code.
L — Liskov Substitution
A subclass must be fully substitutable for its parent. Code that uses the parent must still work when given a child.
Separating via interfaces instead of inheritance is often the right answer.
I — Interface Segregation
Break large interfaces into smaller ones. Don't force implementors to define methods they don't use.
D — Dependency Inversion
Depend on interfaces, not concrete classes. Spring's DI is the practical application of this principle.
No need to memorize — the principles are outcomes
When you write good code consistently, these principles follow naturally. Don't try to memorize the names — just remember the intent behind each one.
☕ Try it yourself — class, inheritance, and polymorphism
🤖 Try asking AI this way
Knowing the concepts in this lesson lets you give AI specific, precise instructions. Not a vague "fix this" but a request backed by vocabulary — and that's where token savings start.
- ▸"Check this class for SOLID violations (especially SRP) and suggest how to split it."
- ▸"Apply the Builder pattern to this object-creation code."
- ▸"Refactor this inheritance structure using interface segregation."
Why does this save tokens?
Without the vocabulary, even after getting an AI response you end up asking "what does that mean?" again. That follow-up question is what eats the tokens. Learn the concepts once and the conversation ends in one go.