What is Object-Oriented Programming (OOP)?
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects—entities that combine data (attributes) and behavior (methods) into cohesive units. Rather than thinking about programs as a sequence of instructions operating on data, OOP encourages us to model software as a collection of interacting objects, each responsible for its own state and behavior.
The power of OOP lies in how closely it mirrors the way humans naturally think about the world. We instinctively categorize things (cars, animals, bank accounts), understand their properties (color, age, balance), and know what actions they can perform (drive, eat, withdraw). OOP allows us to translate this natural way of thinking directly into code, making programs more intuitive to design, understand, and maintain.
Java was designed from the ground up as an object-oriented language, making OOP not just a feature but the fundamental way Java programs are structured. Everything in Java (except primitive types) is an object, and every Java program consists of at least one class. This deep integration means understanding OOP is essential for effective Java development—it's not optional knowledge but the foundation upon which all Java code is built.
Historical Context: The Evolution of OOP
The concepts underlying OOP emerged in the 1960s with Simula, a language designed for simulation that introduced the revolutionary ideas of classes and objects. These ideas were refined and expanded by Smalltalk in the 1970s, which coined the term "object-oriented programming" and introduced concepts like inheritance and dynamic dispatch that remain central to OOP today.
When Java was created in the mid-1990s, its designers drew heavily from these predecessors while learning from the successes and mistakes of C++. Java struck a balance between OOP purity (everything is an object) and pragmatism (primitive types for performance), while adding features like interfaces for flexible abstraction and removing problematic features like multiple inheritance of implementation. The result was a language that made OOP accessible to mainstream developers while providing the structure needed for large-scale software development.
The Big Picture: How OOP Fits in the Java Ecosystem
OOP principles permeate every aspect of Java development. The Java Collections Framework uses inheritance and polymorphism to provide interchangeable data structures. The Java I/O system uses the Decorator pattern (built on inheritance) for flexible stream composition. Frameworks like Spring rely on interfaces and dependency injection (enabled by polymorphism) for loose coupling. Understanding OOP is prerequisite knowledge for effectively using virtually every Java library and framework.
/*
* OOP in the Real World: How Java Applications Are Structured
* ============================================================
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ Java Application │
* │ │
* │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
* │ │ Domain Objects │ │ Services │ │ Controllers │ │
* │ │ │ │ │ │ │ │
* │ │ - User │ │ - UserService │ │ - UserAPI │ │
* │ │ - Order │ │ - OrderService │ │ - OrderAPI │ │
* │ │ - Product │ │ - PaymentSvc │ │ - PaymentAPI │ │
* │ │ │ │ │ │ │ │
* │ │ (Encapsulation) │ │ (Abstraction) │ │ (Polymorphism) │ │
* │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
* │ │ │ │ │
* │ └─────────────────────┼─────────────────────┘ │
* │ │ │
* │ (Inheritance) │
* │ Common behaviors shared │
* │ via base classes/interfaces │
* └─────────────────────────────────────────────────────────────────────┘
*/
Real-World Analogy: Building with LEGO
Imagine you're building with LEGO blocks. Each block is an object that has properties (color, size, shape) and can connect to other blocks in specific ways. You can combine blocks into larger structures (composition), create specialized blocks based on generic ones (inheritance), and use blocks interchangeably when they share the same connection points (polymorphism). Object-Oriented Programming works the same way—we build programs using "objects" that represent real-world things.
A Concrete Example: Modeling a Car
Think about a car in the real world:
- Properties (Data): color, brand, speed, fuel level
- Behaviors (Methods): start(), accelerate(), brake(), refuel()
In OOP, we create a "Car" class (a blueprint) that defines these properties and behaviors. Then we can create many car objects from that blueprint:
public class Car {
// Properties (what a car HAS) - encapsulated as private fields
private String color;
private String brand;
private int speed;
private double fuelLevel;
// Constructor - how cars are created
public Car(String color, String brand) {
this.color = color;
this.brand = brand;
this.speed = 0;
this.fuelLevel = 100.0;
}
// Behaviors (what a car DOES) - public methods
public void accelerate() {
if (fuelLevel > 0) {
speed += 10;
fuelLevel -= 0.5;
System.out.println(brand + " accelerating. Speed: " + speed + " km/h");
} else {
System.out.println("Out of fuel!");
}
}
public void brake() {
speed = Math.max(0, speed - 10);
System.out.println(brand + " braking. Speed: " + speed + " km/h");
}
// Getters provide controlled access to private data
public int getSpeed() { return speed; }
public String getBrand() { return brand; }
}
// Creating actual car objects from the blueprint
Car myCar = new Car("red", "Toyota");
Car yourCar = new Car("blue", "Honda");
myCar.accelerate(); // Toyota accelerating. Speed: 10 km/h
yourCar.accelerate(); // Honda accelerating. Speed: 10 km/h
// Each car has its own speed - they're independent objects
Why Use OOP?
- Organized: Code is grouped logically (all car-related code in one place)
- Reusable: Create many objects from one class (many cars from one Car blueprint)
- Maintainable: Changes to a class are isolated; easy to find and fix problems
- Extensible: New features can be added without breaking existing code
- Mirrors Reality: Code structure matches how we think about the real world
The Four Pillars of OOP
OOP is built on four fundamental principles, often called the "four pillars." These aren't arbitrary rules but time-tested techniques that help us write better, more organized code:
- 1. Encapsulation - "Hide the messy details"
Like a TV remote: you press buttons (simple interface), but you don't see the complex circuit board inside - 2. Inheritance - "Build on what already exists"
Like how a sports car is still a car, but with extra features - 3. Polymorphism - "Many forms, same action"
Like how both dogs and cats can "speak()", but dogs bark and cats meow - 4. Abstraction - "Focus on what, not how"
Like knowing you can "send email" without knowing how the internet works
Let's explore each pillar in detail with practical examples...
1. Encapsulation
Encapsulation is the practice of bundling data (fields) and methods that operate on that data within a single unit (class), and restricting direct access to some of the object's components. It's the most fundamental OOP principle—without encapsulation, objects would just be data containers, and OOP would offer little benefit over procedural programming.
The key insight of encapsulation is that an object should control its own state. Rather than exposing internal data for external code to manipulate directly, an object provides methods that perform operations on its data, ensuring that the object always remains in a valid state.
The Problem Encapsulation Solves
// ❌ BAD: Without encapsulation - data is exposed
public class BankAccountBad {
public double balance; // Anyone can access and modify!
public String accountNumber;
}
// Problems this causes:
BankAccountBad account = new BankAccountBad();
account.balance = -1000000; // Invalid state! Negative balance allowed
account.balance = account.balance * 100; // Oops, multiplied instead of added
account.accountNumber = null; // Now the account has no number!
// No validation, no logging, no security - chaos!
Code Example: Proper Encapsulation
// ✅ GOOD: With encapsulation - data is protected
public class BankAccount {
// Private fields - encapsulated data
private final String accountNumber; // Cannot change once set
private double balance;
private final String owner;
private final List<String> transactionHistory;
// Constructor - controlled initialization
public BankAccount(String accountNumber, String owner) {
if (accountNumber == null || accountNumber.length() != 10) {
throw new IllegalArgumentException("Invalid account number");
}
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = 0.0;
this.transactionHistory = new ArrayList<>();
}
// Getter methods - controlled read access
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
public String getOwner() {
return owner;
}
// Defensive copy - protect internal collection
public List<String> getTransactionHistory() {
return new ArrayList<>(transactionHistory); // Return copy!
}
// Controlled methods with validation
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
balance += amount;
logTransaction("DEPOSIT", amount);
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new IllegalStateException("Insufficient funds");
}
balance -= amount;
logTransaction("WITHDRAWAL", amount);
}
// Private helper - internal implementation detail
private void logTransaction(String type, double amount) {
String entry = String.format("%s: $%.2f | Balance: $%.2f", type, amount, balance);
transactionHistory.add(entry);
}
}
// Usage - the object controls its own state
BankAccount account = new BankAccount("1234567890", "Alice");
account.deposit(1000); // ✅ Validated and logged
account.withdraw(500); // ✅ Validated and logged
// account.balance = -1000; // ❌ Won't compile - balance is private
// account.withdraw(10000); // ❌ Throws exception - insufficient funds
- Data Integrity: Object always remains in a valid state
- Flexibility: Internal implementation can change without affecting external code
- Security: Sensitive data protected from unauthorized access
- Debugging: All modifications go through controlled methods (easier to track)
- Maintainability: Changes isolated to one place
2. Inheritance
Inheritance allows a class to inherit properties and methods from another class, creating a parent-child (or superclass-subclass) relationship. This promotes code reuse and establishes an "is-a" relationship between classes—a Dog is an Animal, a SportsCar is a Car.
Inheritance models the hierarchical relationships we see in the real world. Just as biological taxonomy organizes living things into hierarchies (Animal → Mammal → Dog → Labrador), class inheritance organizes code into hierarchies where more specific classes inherit from more general ones.
Understanding the "is-a" Relationship
/*
* Inheritance Hierarchy
* =====================
*
* ┌─────────────┐
* │ Animal │ ← Base class (most general)
* │ - name │
* │ - eat() │
* │ - sleep() │
* └──────┬──────┘
* │
* ┌───────────────┼───────────────┐
* │ │ │
* ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
* │ Dog │ │ Cat │ │ Bird │
* │ - breed │ │ - indoor │ │ - wingspan │
* │ - bark() │ │ - meow() │ │ - fly() │
* └──────┬──────┘ └─────────────┘ └─────────────┘
* │
* ┌──────┴──────┐
* │ Labrador │ ← Most specific
* │ - fetch() │
* └─────────────┘
*
* Each subclass IS-A type of its parent:
* - Labrador is-a Dog is-a Animal
* - Cat is-a Animal
*/
Code Example
// Parent class (superclass)
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
public String getName() {
return name;
}
}
// Child class (subclass) - inherits from Animal
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Call parent constructor FIRST
this.breed = breed;
}
// Additional method specific to Dog
public void bark() {
System.out.println(name + " says: Woof! Woof!");
}
// Override parent method to provide specialized behavior
@Override
public void eat() {
System.out.println(name + " the " + breed + " is eating dog food");
}
public String getBreed() {
return breed;
}
}
// More specific subclass
public class Labrador extends Dog {
public Labrador(String name, int age) {
super(name, age, "Labrador");
}
public void fetch() {
System.out.println(name + " fetches the ball!");
}
@Override
public void bark() {
System.out.println(name + " says: WOOF! (Labs are loud)");
}
}
// Usage
Dog dog = new Dog("Buddy", 3, "Golden Retriever");
dog.eat(); // Uses overridden method: "Buddy the Golden Retriever is eating dog food"
dog.bark(); // Dog-specific method: "Buddy says: Woof! Woof!"
dog.sleep(); // Inherited method: "Buddy is sleeping"
Labrador lab = new Labrador("Max", 2);
lab.fetch(); // Labrador-specific: "Max fetches the ball!"
lab.bark(); // Overridden: "Max says: WOOF! (Labs are loud)"
lab.eat(); // Inherited from Dog: "Max the Labrador is eating dog food"
While inheritance is powerful, it creates tight coupling between classes. The "is-a" relationship should be genuine and permanent. If you're unsure, consider composition ("has-a" relationship) instead:
// ❌ Problematic inheritance - a Stack is not really a List
public class Stack extends ArrayList { ... } // Exposes unwanted List methods!
// ✅ Better: Composition - Stack uses a List internally
public class Stack<T> {
private List<T> elements = new ArrayList<>(); // has-a List
public void push(T item) { elements.add(item); }
public T pop() { return elements.remove(elements.size() - 1); }
}
- Java supports only single inheritance (one parent class)
- Use
extendskeyword to inherit - Use
superto access parent members - Constructor chaining: subclass constructor must call superclass constructor
- All classes implicitly extend
Objectif no parent specified - Use interfaces for multiple inheritance of type
3. Polymorphism
Polymorphism means "many forms." It allows objects of different classes to be treated as objects of a common parent class, with each responding to the same method call in its own way. This is perhaps OOP's most powerful feature—it enables writing flexible, extensible code that works with objects based on what they can do rather than what they are.
Polymorphism comes in two forms in Java:
- Compile-time (Static) Polymorphism: Method overloading - same method name, different parameters
- Runtime (Dynamic) Polymorphism: Method overriding - subclass provides specific implementation
Runtime Polymorphism in Action
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method - each shape calculates area differently
public abstract double calculateArea();
public void displayInfo() {
System.out.println(color + " shape with area: " + calculateArea());
}
}
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
public class Triangle extends Shape {
private double base, height;
public Triangle(String color, double base, double height) {
super(color);
this.base = base;
this.height = height;
}
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
// THE POWER OF POLYMORPHISM:
// One method works with ANY shape - present or FUTURE!
public class ShapeCalculator {
// This method works with Circle, Rectangle, Triangle, and any
// future Shape subclass without modification!
public static double calculateTotalArea(List<Shape> shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.calculateArea(); // Calls the RIGHT method at runtime
}
return total;
}
public static void main(String[] args) {
List<Shape> shapes = Arrays.asList(
new Circle("Red", 5.0),
new Rectangle("Blue", 4.0, 6.0),
new Triangle("Green", 3.0, 4.0)
);
// Each shape calculates its area using ITS OWN formula
for (Shape shape : shapes) {
shape.displayInfo();
}
// Output:
// Red shape with area: 78.54...
// Blue shape with area: 24.0
// Green shape with area: 6.0
System.out.println("Total area: " + calculateTotalArea(shapes));
}
}
Compile-Time Polymorphism (Method Overloading)
public class Calculator {
// Same method name, different parameters
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public String add(String a, String b) {
return a + b; // String concatenation
}
}
Calculator calc = new Calculator();
calc.add(5, 10); // Calls int version: 15
calc.add(3.14, 2.86); // Calls double version: 6.0
calc.add(1, 2, 3); // Calls three-param version: 6
calc.add("Hello", "World"); // Calls String version: "HelloWorld"
Polymorphism enables plugin architectures where new functionality can be added without modifying existing code:
public interface PaymentProcessor {
void processPayment(double amount);
}
// Existing processors
public class CreditCardProcessor implements PaymentProcessor { ... }
public class PayPalProcessor implements PaymentProcessor { ... }
// Add new payment method WITHOUT changing PaymentService!
public class CryptoProcessor implements PaymentProcessor { ... }
public class PaymentService {
public void pay(PaymentProcessor processor, double amount) {
processor.processPayment(amount); // Works with ANY processor
}
}
4. Abstraction
Abstraction means hiding complex implementation details and showing only the essential features. It focuses on WHAT an object does rather than HOW it does it. While encapsulation hides data, abstraction hides complexity.
Think about driving a car: you interact with a simple interface (steering wheel, pedals, gear shift) without needing to understand the complex mechanics underneath. The car abstracts away the complexity of internal combustion, transmission systems, and electronic controls.
Abstraction in Java
Java provides two mechanisms for abstraction:
- Abstract Classes: Partially implemented classes that define a template
- Interfaces: Fully abstract contracts that define capabilities
Code Example: Payment Processing System
// Abstract class provides template with some implementation
public abstract class Payment {
protected double amount;
protected String transactionId;
protected LocalDateTime timestamp;
public Payment(double amount) {
this.amount = amount;
this.transactionId = generateTransactionId();
this.timestamp = LocalDateTime.now();
}
// Abstract methods - MUST be implemented by subclasses
// These define WHAT must happen, not HOW
public abstract boolean validate();
public abstract void executePayment();
public abstract void sendConfirmation();
// Concrete method - shared implementation
public final void process() {
// Template Method Pattern: defines the algorithm structure
System.out.println("Starting payment processing...");
if (validate()) {
executePayment();
sendConfirmation();
printReceipt();
} else {
System.out.println("Payment validation failed");
}
}
protected void printReceipt() {
System.out.println("Receipt:");
System.out.println(" Transaction ID: " + transactionId);
System.out.println(" Amount: $" + amount);
System.out.println(" Time: " + timestamp);
}
private String generateTransactionId() {
return "TXN-" + System.currentTimeMillis();
}
}
// Concrete implementation - provides the HOW
public class CreditCardPayment extends Payment {
private String cardNumber;
private String cvv;
private String expiryDate;
public CreditCardPayment(double amount, String cardNumber,
String cvv, String expiryDate) {
super(amount);
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
@Override
public boolean validate() {
// Credit card specific validation
return cardNumber != null
&& cardNumber.length() == 16
&& cvv.length() == 3
&& !isExpired();
}
@Override
public void executePayment() {
System.out.println("Charging credit card ending in "
+ cardNumber.substring(12) + " for $" + amount);
// Connect to payment gateway, process charge, etc.
}
@Override
public void sendConfirmation() {
System.out.println("Sending email confirmation for credit card payment");
}
private boolean isExpired() {
// Check if card is expired
return false;
}
}
// Another concrete implementation
public class PayPalPayment extends Payment {
private String email;
private String authToken;
public PayPalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public boolean validate() {
// PayPal specific validation
return email != null && email.contains("@") && authenticateUser();
}
@Override
public void executePayment() {
System.out.println("Processing PayPal payment for " + email);
// Connect to PayPal API, authorize, transfer funds
}
@Override
public void sendConfirmation() {
System.out.println("Sending PayPal notification to " + email);
}
private boolean authenticateUser() {
// OAuth authentication with PayPal
return true;
}
}
// Usage - client code doesn't need to know implementation details
public class PaymentDemo {
public static void main(String[] args) {
// Process ANY payment type with the same code
Payment payment1 = new CreditCardPayment(99.99, "1234567890123456", "123", "12/25");
Payment payment2 = new PayPalPayment(49.99, "user@example.com");
payment1.process(); // Uses credit card implementation
payment2.process(); // Uses PayPal implementation
}
}
| Use Abstract Class When | Use Interface When |
|---|---|
| You need to share code among related classes | Unrelated classes need same capability |
| You need constructors or instance fields | You want multiple inheritance of type |
| You want to provide default implementations | You want to define a contract only |
| Example: Animal, Vehicle, Payment | Example: Comparable, Serializable, Runnable |
See Interfaces vs Abstract Classes for detailed comparison.
Under the Hood: How OOP Works in the JVM
Understanding how the JVM implements OOP concepts helps you write better code and debug issues more effectively.
Method Dispatch: How Polymorphism Works
/*
* Virtual Method Table (vtable)
* =============================
*
* Each class has a vtable - a table of pointers to method implementations.
* When you call a method on an object, the JVM:
* 1. Looks at the object's actual class (not the reference type)
* 2. Finds the method in that class's vtable
* 3. Calls the correct implementation
*
* Shape shape = new Circle();
* shape.calculateArea(); // Which method is called?
*
* Step 1: shape refers to a Circle object
* Step 2: JVM looks in Circle's vtable for calculateArea
* Step 3: Finds Circle's override, calls it
*
* ┌─────────────────────────────┐
* │ Circle instance (on heap) │
* │ - class pointer ──────────┼───► Circle.class vtable:
* │ - radius: 5.0 │ calculateArea() ─► Circle's impl
* │ - color: "Red" │ displayInfo() ─► Shape's impl
* └─────────────────────────────┘
*/
// This is why the "real" type matters, not the reference type:
Shape shape = new Circle("Red", 5.0); // Reference type: Shape, Object type: Circle
shape.calculateArea(); // Calls Circle's version (runtime polymorphism)
Object Memory Layout
/*
* Object Layout in Memory
* =======================
*
* Dog dog = new Dog("Buddy", 3, "Labrador");
*
* ┌──────────────────────────────────────────┐
* │ Dog Object (Heap) │
* ├──────────────────────────────────────────┤
* │ Object Header (12-16 bytes) │
* │ - Mark word (hashCode, locks, GC info) │
* │ - Class pointer → Dog.class │
* ├──────────────────────────────────────────┤
* │ Animal fields (inherited) │
* │ - name: "Buddy" (reference to String) │
* │ - age: 3 (int, 4 bytes) │
* ├──────────────────────────────────────────┤
* │ Dog fields (this class) │
* │ - breed: "Labrador" (reference) │
* └──────────────────────────────────────────┘
*
* Inheritance = fields are laid out in order from parent to child
*/
Best Practices
Encapsulation Best Practices
- Make fields private - Always. No exceptions.
- Provide getters only when needed - Don't auto-generate for everything
- Be cautious with setters - Consider immutability; use builders or factory methods
- Return defensive copies - For mutable objects like collections, dates
- Validate in setters/constructors - Never allow invalid state
Inheritance Best Practices
- Favor composition over inheritance - Use "has-a" when "is-a" isn't genuine
- Keep hierarchies shallow - 2-3 levels maximum
- Use
finalto prevent inheritance - When a class shouldn't be extended - Document for inheritance or prohibit it - Design explicitly for extension
- Don't inherit for code reuse alone - The relationship must make semantic sense
Polymorphism Best Practices
- Program to interfaces, not implementations - Use abstract types in declarations
- Follow Liskov Substitution Principle - Subtypes must be substitutable for base types
- Use
@Overrideannotation - Compiler catches mistakes - Avoid type checking with instanceof - Usually indicates design problem
Abstraction Best Practices
- Keep interfaces small and focused - Single responsibility
- Use abstract classes for template patterns - When sharing implementation
- Don't leak implementation details - Interface should describe capability
- Design contracts carefully - Once published, hard to change
// ✅ GOOD: Programming to interfaces
List<String> names = new ArrayList<>(); // Can switch to LinkedList easily
Map<String, Integer> scores = new HashMap<>();
// ❌ BAD: Programming to implementations
ArrayList<String> names = new ArrayList<>(); // Locked into ArrayList
// ✅ GOOD: Method accepts interface type
public void processItems(Collection<Item> items) { ... }
// ❌ BAD: Method accepts concrete type
public void processItems(ArrayList<Item> items) { ... }
Common Pitfalls
// ❌ BAD: Unnecessary deep hierarchy
class LivingThing { }
class Animal extends LivingThing { }
class Mammal extends Animal { }
class Canine extends Mammal { }
class Dog extends Canine { }
class Labrador extends Dog { } // 6 levels deep!
// ✅ BETTER: Flatter hierarchy with composition
class Dog {
private Breed breed; // Composition
private BehaviorSet behaviors; // Composition
}
// ❌ BAD: Exposing mutable internal state
public class Team {
private List<Player> players = new ArrayList<>();
public List<Player> getPlayers() {
return players; // Caller can modify!
}
}
team.getPlayers().clear(); // Oops! Destroyed the team
// ✅ GOOD: Return defensive copy or unmodifiable view
public List<Player> getPlayers() {
return Collections.unmodifiableList(players);
// OR: return new ArrayList<>(players);
}
// ❌ BAD: Square "is-a" Rectangle? Not really...
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = w;
height = w; // Must keep square!
}
@Override
public void setHeight(int h) {
width = h;
height = h;
}
}
// This breaks expectations:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.getArea()); // Expects 50, gets 100!
// ✅ BETTER: Don't use inheritance here
interface Shape { int getArea(); }
class Rectangle implements Shape { ... }
class Square implements Shape { ... } // Independent implementations
// ❌ BAD: Typo creates new method instead of overriding
class Dog extends Animal {
public void eats() { // Typo! Should be eat()
System.out.println("Dog eating");
}
}
Animal dog = new Dog();
dog.eat(); // Calls Animal.eat(), not our "override"!
// ✅ GOOD: @Override catches the mistake at compile time
class Dog extends Animal {
@Override // Compiler error: method does not override
public void eats() { ... }
}
Performance Considerations
OOP adds minimal overhead in modern JVMs:
- Virtual method calls: ~1 nanosecond overhead (negligible in most cases)
- Object creation: Optimized by JVM; ~10 nanoseconds for small objects
- Inheritance depth: No runtime cost; field offsets calculated at class loading
Focus on good design first. Optimize only when profiling shows a bottleneck.
// Performance myths debunked:
// MYTH: "Deep inheritance is slow"
// REALITY: Field access is O(1) - offsets are precomputed
Labrador lab = new Labrador("Max", 3);
lab.getName(); // Same speed whether name is in Animal or Labrador
// MYTH: "Interface calls are slower than class calls"
// REALITY: JIT optimizes common cases (monomorphic/bimorphic sites)
List<String> list = new ArrayList<>();
list.add("item"); // JIT often inlines this
// WHEN TO WORRY:
// - Extremely hot loops (millions of iterations)
// - Real-time systems with strict latency requirements
// - Even then, measure first!
In Practice: Real-World Applications
Java Collections Framework
// The Collections Framework is a masterclass in OOP design:
// ABSTRACTION: Interface defines contract
public interface List<E> extends Collection<E> {
void add(int index, E element);
E get(int index);
// ...
}
// INHERITANCE: AbstractList provides common implementation
public abstract class AbstractList<E> implements List<E> {
// Provides iterator(), indexOf(), etc.
}
// POLYMORPHISM: Different implementations, same interface
List<String> arrayList = new ArrayList<>(); // O(1) random access
List<String> linkedList = new LinkedList<>(); // O(1) insertion
// ENCAPSULATION: Internal array/nodes hidden
arrayList.add("item"); // Don't know/care about internal array resizing
Spring Framework
// Spring uses OOP principles extensively:
// ABSTRACTION + POLYMORPHISM: Dependency Injection
@Service
public class OrderService {
private final PaymentProcessor paymentProcessor; // Interface!
// Spring injects the appropriate implementation
public OrderService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void processOrder(Order order) {
paymentProcessor.process(order.getTotal()); // Works with ANY processor
}
}
// Easy to swap implementations:
@Profile("production")
@Component
public class StripePaymentProcessor implements PaymentProcessor { }
@Profile("test")
@Component
public class MockPaymentProcessor implements PaymentProcessor { }
Troubleshooting Guide
Error: "Cannot instantiate abstract class"
// Problem:
Animal animal = new Animal("Rex", 5); // ERROR!
// Solution: Create a concrete subclass instance
Animal animal = new Dog("Rex", 5, "Labrador"); // ✅
Error: "Method does not override method from superclass"
// Problem: Signature doesn't match
class Dog extends Animal {
@Override
public void eat(String food) { } // Different signature!
}
// Solution: Match the parent method signature exactly
@Override
public void eat() { } // ✅ Matches Animal.eat()
Error: "Constructor call must be first statement"
// Problem: super() not first
public Dog(String name) {
System.out.println("Creating dog"); // ERROR!
super(name, 0);
}
// Solution: super() must be first
public Dog(String name) {
super(name, 0); // ✅ First!
System.out.println("Creating dog");
}
Problem: Unexpected method called (wrong polymorphism)
// Problem: Calling overloaded instead of overridden method
class Parent {
public void process(Object obj) {
System.out.println("Parent");
}
}
class Child extends Parent {
public void process(String str) { // Overloading, not overriding!
System.out.println("Child");
}
}
Parent p = new Child();
p.process("test"); // Prints "Parent"! Reference type determines overload
// Solution: Make sure you're overriding, not overloading
class Child extends Parent {
@Override
public void process(Object obj) { // Same signature = override
System.out.println("Child");
}
}
Interview Questions
Answer: Encapsulation (hiding data, exposing behavior), Inheritance (creating class hierarchies, code reuse), Polymorphism (many forms, runtime method dispatch), and Abstraction (hiding complexity, showing essential features). Together they enable modular, maintainable, extensible code.
Answer: Overloading is compile-time polymorphism where multiple methods have the same name but different parameters (resolved by compiler based on argument types). Overriding is runtime polymorphism where a subclass provides a specific implementation of a parent method (same signature, resolved at runtime based on object type).
Answer: Inheritance creates tight coupling and exposes internal details. Composition is more flexible (can change at runtime), avoids fragile base class problem, and better models "has-a" relationships. Use inheritance only for genuine "is-a" relationships where substitutability is needed.
Answer: Objects of a superclass should be replaceable with objects of a subclass without altering program correctness. Subclasses must honor the contract of the parent: accept the same inputs (or more general), return the same outputs (or more specific), and not throw unexpected exceptions. The classic Square/Rectangle example violates this.
Answer: No. Static methods belong to the class, not instances, so there's no polymorphism. If a subclass defines a static method with the same signature, it's method hiding, not overriding. The method called depends on the reference type, not the object type.
Answer: The diamond problem occurs when a class inherits from two classes that have a common ancestor, causing ambiguity about which inherited member to use. Java avoids this by allowing only single inheritance for classes. For interfaces (Java 8+), if two interfaces have default methods with the same signature, the implementing class must override and explicitly choose which to use.
Answer: Encapsulation hides data (making fields private, providing controlled access). Abstraction hides implementation complexity (showing what an object does, not how). Encapsulation is about bundling and protecting; abstraction is about simplifying the interface. They're complementary: encapsulation implements data hiding, abstraction defines what to hide.
Answer: Use abstract class when: sharing code among related classes, need constructors/instance fields, defining template methods. Use interface when: defining a capability for unrelated classes, need multiple inheritance of type, defining pure contracts. Java 8+ blurred this with default methods in interfaces, but abstract classes still win when you need state or constructors.
See Also
- Interfaces vs Abstract Classes – Detailed comparison and when to use each
- Access Modifiers – Implementing encapsulation
- Constructors – Object initialization and inheritance
- Design Patterns – OOP patterns in practice
- SOLID Principles – OOP design principles