Java Academy

Master Java Development

Menu

Core Java

Servlets

JDBC

Microservices

Build Tools

APIs

Core Java · Lesson 6

Object-Oriented Programming

The Four Pillars of Object-Oriented Programming

Object-Oriented Programming (OOP) is a design philosophy that organizes code around objects and how they interact. The four core principles are Encapsulation (hiding internal details), Inheritance (sharing behavior through class hierarchies), Polymorphism (objects of different types responding to the same request), and Abstraction (showing only essential features). Mastering these principles makes your code reusable, maintainable, and flexible.

Encapsulation: Bundling Data and Methods

Encapsulation hides internal implementation details and exposes only what's necessary. Use private for attributes and internal methods, and public for the interface others interact with. This protects data from invalid states — for example, a BankAccount can prevent negative balances by hiding the balance field and controlling access through validated deposit/withdraw methods.

public class BankAccount {
    private String accountNumber;
    private double balance;  // private: only this class can access
    
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    
    // public: others can call this
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    
    public double getBalance() {
        return balance;
    }
    
    // private: only this class can call
    private void logTransaction(String type, double amount) {
        System.out.println("[LOG] " + type + ": $" + amount);
    }
}

Inheritance: Reusing Code Through Class Hierarchies

Inheritance lets one class (subclass) inherit attributes and methods from another (superclass), promoting code reuse. Use the extends keyword to create a subclass. The subclass inherits everything from the superclass and can add its own unique attributes and methods. This prevents duplicating common code across related classes.

// Superclass
public class Animal {
    protected String name;  // protected: subclasses can access
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void makeSound() {
        System.out.println(name + " makes a sound");
    }
    
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Subclass 1
public class Dog extends Animal {
    public Dog(String name) {
        super(name);  // call parent constructor
    }
    
    @Override  // override parent method
    public void makeSound() {
        System.out.println(name + " barks: Woof!");
    }
}

// Subclass 2
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " meows: Meow!");
    }
}

class InheritanceDemo {
    public static void main(String[] args) {
        Animal dog = new Dog("Buddy");
        Animal cat = new Cat("Whiskers");
        
        dog.makeSound();  // Buddy barks: Woof!
        cat.makeSound();  // Whiskers meows: Meow!
        
        dog.eat();  // Buddy is eating (inherited from Animal)
        cat.eat();  // Whiskers is eating
    }
}

Polymorphism: One Interface, Many Forms

Polymorphism means "many forms" — the same method call behaves differently depending on the object type. The makeSound() method exists in the parent class, but each subclass (Dog, Cat) provides its own implementation. This lets you write flexible code: call makeSound() on any Animal without knowing its specific type.

public class PolymorphismDemo {
    public static void main(String[] args) {
        // Array of Animal references (can hold any Animal subclass)
        Animal[] animals = {
            new Dog("Rex"),
            new Cat("Mittens"),
            new Dog("Poodle"),
            new Cat("Fluffy")
        };
        
        // Same method call, different behavior for each animal
        for (Animal animal : animals) {
            animal.makeSound();  // Calls Dog.makeSound() or Cat.makeSound()
        }
        
        // Polymorphism in action: one method, multiple outcomes
        System.out.println("\nAll animals eating:");
        for (Animal animal : animals) {
            animal.eat();  // All use inherited eat() method
        }
    }
}

Abstraction: Focusing on What, Not How

Abstraction simplifies complex systems by hiding unnecessary details and showing only essential features. In Java, you achieve this with abstract classes and interfaces. An abstract class uses the abstract keyword to define methods that subclasses must implement, establishing a contract. You cannot instantiate an abstract class directly; you must create concrete subclasses.

// Abstract class: defines what subclasses must do
public abstract class Vehicle {
    protected String brand;
    
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    // abstract method: subclasses must provide implementation
    public abstract void start();
    public abstract void stop();
    
    // concrete method: same for all vehicles
    public void honk() {
        System.out.println(brand + " vehicle goes: Beep!");
    }
}

public class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }
    
    @Override
    public void start() {
        System.out.println(brand + " car engine started");
    }
    
    @Override
    public void stop() {
        System.out.println(brand + " car engine stopped");
    }
}

public class Motorcycle extends Vehicle {
    public Motorcycle(String brand) {
        super(brand);
    }
    
    @Override
    public void start() {
        System.out.println(brand + " motorcycle ignition on");
    }
    
    @Override
    public void stop() {
        System.out.println(brand + " motorcycle ignition off");
    }
}

class AbstractionDemo {
    public static void main(String[] args) {
        // Vehicle vehicle = new Vehicle();  // ERROR: can't instantiate abstract class
        
        Vehicle car = new Car("Toyota");
        Vehicle bike = new Motorcycle("Harley");
        
        car.start();   // Toyota car engine started
        bike.start();  // Harley motorcycle ignition on
        
        car.honk();    // Toyota vehicle goes: Beep! (inherited)
    }
}
OOP PrinciplePurposeExample
EncapsulationHide internal details, protect dataPrivate attributes with public getters/setters
InheritanceShare code across related classesDog extends Animal
PolymorphismSame method, different behaviormakeSound() in Dog vs Cat
AbstractionShow only essential features, hide complexityAbstract class Vehicle with required methods

Common Mistake: Using inheritance when composition would be better. Inheritance should represent an "is-a" relationship (Dog IS-A Animal). Don't inherit just to reuse code; if it's a "has-a" relationship (Car HAS-A Engine), use composition (a field) instead. Over-using inheritance creates rigid, hard-to-maintain hierarchies.

Best Practices for Object-Oriented Design

  • Apply SOLID principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
  • Prefer composition over inheritance when it makes sense; it's more flexible
  • Use abstract classes for shared behavior, interfaces for contracts
  • Design for interfaces, not implementations: reduces coupling between classes
  • Keep inheritance hierarchies shallow: deep hierarchies are harder to understand
  • Override toString(), equals(), and hashCode() in your classes for better usability
  • Use @Override annotation to catch mistakes if a parent method signature changes

Real-World Example: E-Commerce Product Catalog

An online store models products with an abstract Product class defining getPrice(), applyDiscount(), and getDescription(). Concrete subclasses like PhysicalProduct, DigitalProduct, and Subscription override these methods differently (physical items have shipping, digital items don't). A shopping cart accepts any Product through polymorphism. When calculating totals, the cart calls getPrice() on each item without caring about its specific type. This design is extensible: adding a new product type is as simple as creating a new subclass, without changing the cart logic. The architecture demonstrates all four OOP principles working together to create flexible, maintainable code.

Quiz Check

Quick Knowledge Test

Loading quiz questions...