OOP, re-examined.
You know what classes and inheritance are. Today is the question your tutorial never answered: when do I actually pick an abstract class vs. an interface? What is a marker interface and why does Serializable exist? Why does everyone preach 'composition over inheritance'?
Why OOP at all? — the 30-second story
Before OOP (1980s and earlier), code was organized as functions over data structures (procedural). For a 5,000-line program, fine. For a 500,000-line program, you couldn't tell what depended on what. Changes anywhere broke things everywhere.
OOP's bet: bind data and the operations on it together into "objects." Hide the internals. Let objects delegate to other objects. The result: large programs become tractable because each object is its own little world with a public API.
OOP is not primarily about inheritance or polymorphism. It is about encapsulation — hiding state behind a public interface so that changes inside one object cannot ripple through the entire codebase. Everything else (inheritance, polymorphism, abstraction) supports this one goal.
Decision 1 · Abstract class vs. Interface
This is the most-asked OOP interview question. Every learner gets a vague answer. Here is the precise one.
Abstract class
- Use when you have a partial implementation to share + want subclasses to fill in gaps
- Can have state (instance fields)
- Can have constructors
- A class can extend only one abstract class
- Methods can be
protected/ private - Best for: "is-a" relationships in a single hierarchy
Interface
- Use to define a capability or contract — what an object can do
- Cannot hold mutable state (only constants)
- No constructors
- A class can implement many interfaces
- All methods
publicby default - Best for: "can-do" roles, decoupling, mocking
The simple decision rule
- Will I share code (real method bodies) or just a contract?
- Do classes that satisfy this need to fit into one hierarchy, or play many roles?
Need shared code + single hierarchy → abstract class.
Need a contract + classes might play many roles → interface.
Since Java 8, interfaces can have default methods (with bodies). This blurred the line — but the state rule still holds: only abstract classes can have mutable instance fields. If you need state, you need an abstract class.
Decision 2 · Marker interfaces — what and why
A marker interface is an interface with no methods. Empty. So why have it?
The classic example is java.io.Serializable. It is empty:
public interface Serializable {
// nothing here
}Yet putting implements Serializable on your class changes how it's treated by the JVM. Other code (like ObjectOutputStream) checks at runtime: "does this object implement Serializable?" If yes — proceed. If no — throw NotSerializableException.
The interface is a flag, a yes/no marker. Hence "marker."
Common marker interfaces
Serializable— "I can be turned into bytes"Cloneable— "you can callclone()on me"RandomAccess— "I support O(1) index access" (used byArrayList)
Modern alternative — annotations
- Since Java 5, annotations like
@Entity,@Componentserve a similar role - More flexible: can carry parameters
- Marker interfaces persist mainly for type safety (compiler can check)
- For new code, prefer annotations
Decision 3 · Composition over inheritance
This is the most repeated principle in modern OOP. It means: prefer holding an object as a field over extending its class.
Why?
Inheritance creates a permanent, compile-time bond. Once B extends A, every public method of A becomes part of B's API. Change A, you risk breaking B. This is called the fragile base class problem.
Composition (B has a field of type A) is loose. B exposes only what it wants. B can swap A for a different implementation at runtime.
Inheritance — rigid
class Logger {
void log(String s) {...}
}
class UserService extends Logger {
// inherits log()
// also inherits ALL
// other Logger methods
}Composition — flexible
class UserService {
private final Logger logger;
UserService(Logger l) {
this.logger = l;
}
// expose only what's needed
// swap logger easily
}Spring's entire dependency-injection model is composition taken seriously. @Autowired injects a dependency as a field — never via inheritance. Modern Java backend code uses inheritance sparingly (mostly within a small framework hierarchy) and composition heavily.
Decision 4 · The four pillars — what really matters
Tutorials list four pillars: encapsulation, inheritance, polymorphism, abstraction. Most learners memorize the words. Let me reframe by importance for daily work:
| Pillar | How often you'll use it | Why it matters |
|---|---|---|
| Encapsulation | Every class | Private fields + getters/setters. Lombok's @Getter/@Setter. The default. |
| Polymorphism | Every framework method | Spring passes you a List, you don't know if it's ArrayList or something else. That's polymorphism. |
| Abstraction | Every layer | Your service depends on a UserRepository interface, not a concrete class. Lets Spring swap implementations. |
| Inheritance | Rarely (in your code) | You'll extends framework classes (extends RuntimeException) but not your own often. Composition wins. |
Polymorphism — the practical version
"One name, many forms." But what does it mean in code you'll write?
// You declare with the abstract type
List<User> users = userRepository.findAll();
// Spring Data returns... ArrayList? Hibernate's PersistentBag?
// You don't care. You can call .size(), .get(0), .stream()
// because all of them satisfy the List contract.Polymorphism's daily payoff: your code depends on contracts, not concrete types. If Spring switches the implementation tomorrow, your code keeps working.
Common OOP mistakes I see in learners
- Inheriting just to share helper methods. Use composition or static utilities instead.
- Putting business logic in DTOs. DTOs should be data carriers. Logic goes in services.
- Public fields. Always private + getter (or use
recordfor value objects in modern Java). - Abstract class with zero abstract methods. If the parent has no abstract methods, why is it abstract? Probably should be a regular class or split into an interface + helper.
- Deep inheritance trees (3+ levels). Almost always a sign that composition was the right choice.
Quick refresh — concepts you asked about
Abstract class, abstract method
A class marked abstract cannot be instantiated. It exists to be extended. Methods marked abstract have no body and force subclasses to implement them.
Final class / final method
final on a class = cannot be subclassed. final on a method = cannot be overridden. Used to lock down design intent. String is final — that's why nobody can subclass it.
Static
Belongs to the class, not an instance. Math.max() doesn't need a Math object. Static methods can't access instance fields.
this vs super
this = the current instance. super = the parent class part of the current instance. super.method() calls the parent's version.
Lock in today's learning
If any answer is fuzzy, that's the section you re-read.
- Give a concrete example where you'd choose an abstract class over an interface, and one for the reverse.
- Why is
Serializableempty? What does that empty interface actually do? - Explain "composition over inheritance" using your own example.
- What does polymorphism let you do in everyday Spring code?
- Why is encapsulation the most important OOP pillar — even more than the others?
- What problem does the fragile base class describe, and which OOP rule fixes it?
End of Day 3. Tomorrow: Collections internals — the most-used and most-misunderstood part of Java.