WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 04: OOP Deep Dive

Go deeper into OOP pillars: encapsulation, inheritance, polymorphism, and interfaces. Build safer, reusable, and flexible designs.

Introduction

Now that you know the basics of classes and objects, let's explore the four core principles that make object-oriented programming powerful. Encapsulation keeps your data safe by controlling how others can access it. Inheritance lets you reuse code by building new classes on top of existing ones. Polymorphism allows different objects to respond to the same command in their own way. Interfaces create contracts that multiple classes can follow. These concepts help you write code that is safer, easier to maintain, and more flexible. Think of them as tools that help you organize your code like a well-structured building.

Encapsulation

Definition

Encapsulation means hiding the internal details of an object and only allowing controlled access through specific methods. You make fields private and create public methods (getters and setters) to read or modify them safely. This protects your data from being changed in ways that could break your program.

Analogy

Think of your bank account. You can't directly reach into the bank's computer and change your balance. Instead, you use an ATM or app that provides safe ways to deposit, withdraw, or check your balance. The bank protects your account details and only allows you to interact through secure, controlled methods. If there's not enough money, the withdrawal is denied. This prevents impossible situations like having a negative balance when that's not allowed.

Example

// BankAccount class with encapsulation
class BankAccount {                      // class definition
  private double balance;                // private field - hidden from outside
  private String accountNumber;          // private field - protected data

  // Constructor to set up account
  public BankAccount(String accountNumber, double initialBalance) {
    this.accountNumber = accountNumber;  // set account number
    this.balance = initialBalance;       // set starting balance
  }

  // Getter method - safe way to read balance
  public double getBalance() {           // public method to access private data
    return balance;                      // return current balance
  }

  // Safe way to add money
  public void deposit(double amount) {   // public method to modify balance
    if (amount > 0) {                    // validate input - no negative deposits
      balance += amount;                 // safely update balance
      System.out.println("Deposited: $" + amount);
    } else {
      System.out.println("Invalid deposit amount");
    }
  }

  // Safe way to remove money
  public boolean withdraw(double amount) { // returns success/failure
    if (amount > 0 && amount <= balance) { // check if withdrawal is valid
      balance -= amount;                   // safely reduce balance
      System.out.println("Withdrew: $" + amount);
      return true;                         // indicate success
    } else {
      System.out.println("Invalid withdrawal");
      return false;                        // indicate failure
    }
  }
}

Inheritance

Definition

Inheritance allows you to create a new class based on an existing class. The new class (child) automatically gets all the fields and methods from the parent class, and can add its own features or modify existing ones. This helps you reuse code and create specialized versions of general concepts.

Analogy

Inheritance is like a child inheriting traits from their parents. A child might inherit their parent's eye color, height, and certain abilities, but they also develop their own unique skills and personality traits. In the same way, a child class inherits everything from its parent class but can add new features or change how certain things work. Just like how all family members share some common characteristics but each person is still unique.

Example

// Base class representing any vehicle
class Vehicle {                          // parent class
  protected String brand;                // field accessible to children
  protected int maxSpeed;                // field accessible to children

  public Vehicle(String brand, int maxSpeed) { // constructor
    this.brand = brand;                  // set brand
    this.maxSpeed = maxSpeed;            // set maximum speed
  }

  public void start() {                  // method inherited by children
    System.out.println(brand + " is starting...");
  }

  public void move() {                   // method inherited by children
    System.out.println("Moving at speed up to " + maxSpeed + " mph");
  }
}

// Specialized class that inherits from Vehicle
class Car extends Vehicle {             // child class inheriting from Vehicle
  private int doors;                    // additional field specific to cars

  public Car(String brand, int maxSpeed, int doors) { // constructor
    super(brand, maxSpeed);             // call parent constructor
    this.doors = doors;                 // set car-specific field
  }

  public void honk() {                  // new method only cars have
    System.out.println("Beep beep!");  // car-specific behavior
  }

  // Override parent method with car-specific version
  @Override
  public void move() {                  // customize inherited method
    System.out.println("Driving on roads at " + maxSpeed + " mph");
  }
}

// Another specialized class
class Motorcycle extends Vehicle {       // another child class
  public Motorcycle(String brand, int maxSpeed) {
    super(brand, maxSpeed);             // call parent constructor
  }

  public void wheelie() {               // motorcycle-specific method
    System.out.println("Doing a wheelie!");
  }

  @Override
  public void move() {                  // customize inherited method
    System.out.println("Riding on two wheels at " + maxSpeed + " mph");
  }
}

Polymorphism

Definition

Polymorphism allows objects of different classes to be treated as objects of a common parent class, while each object behaves according to its own specific type. When you call a method on a parent reference, the actual method that runs depends on what type of object it really is. This lets you write flexible code that works with many different types.

Analogy

Polymorphism is like one person playing multiple roles. The same person might be a student at school, a child at home, and an employee at work. Depending on the situation, they behave differently, but they're still the same person. When someone says "student, please raise your hand," all students respond, but each in their own way - some are eager, others reluctant. The command is the same, but each person's response reflects their individual personality and circumstances.

Example

// Using the Vehicle hierarchy from inheritance example
class VehicleDemo {                     // demonstration class
  // Method that works with any Vehicle type
  public static void testVehicle(Vehicle v) { // accepts any Vehicle or its children
    v.start();                          // calls appropriate start method
    v.move();                           // calls the specific move() version
    System.out.println("---");
  }

  public static void main(String[] args) {
    // Create different types of vehicles
    Vehicle car = new Car("Toyota", 120, 4);        // Car object in Vehicle reference
    Vehicle motorcycle = new Motorcycle("Honda", 180); // Motorcycle in Vehicle reference
    Vehicle genericVehicle = new Vehicle("Generic", 50); // Regular Vehicle object

    // All calls to testVehicle work the same way
    testVehicle(car);                   // calls Car's move() method
    testVehicle(motorcycle);            // calls Motorcycle's move() method
    testVehicle(genericVehicle);        // calls Vehicle's move() method

    // Array of different vehicle types
    Vehicle[] fleet = {                 // array holding different types
      new Car("BMW", 150, 2),
      new Motorcycle("Yamaha", 200),
      new Car("Ford", 110, 4)
    };

    // Process all vehicles the same way
    for (Vehicle vehicle : fleet) {     // iterate through mixed types
      vehicle.start();                  // correct start() method called
      vehicle.move();                   // correct move() method called
    }
  }
}

Interfaces

Definition

An interface is a contract that defines what methods a class must have, without specifying how those methods work. Any class that implements an interface must provide its own version of all the interface's methods. Interfaces allow different classes to be used interchangeably as long as they follow the same contract.

Analogy

Think of a driver's license test. Everyone who wants to drive must demonstrate they can start the car, steer, brake, and park. The test doesn't care what kind of car you use or exactly how you perform these actions - it just ensures you can do the required tasks. Once you pass, you can drive any car because all cars follow the same basic "driving interface." Different car manufacturers implement steering and braking differently, but they all respond to the same basic commands from the driver.

Example

// Interface defining what payment methods must do
interface PaymentProcessor {            // contract definition
  boolean processPayment(double amount); // required method signature
  String getPaymentType();              // required method signature
}

// Credit card implementation
class CreditCardProcessor implements PaymentProcessor { // implements the contract
  private String cardNumber;            // specific to credit cards

  public CreditCardProcessor(String cardNumber) {
    this.cardNumber = cardNumber;       // set card details
  }

  @Override
  public boolean processPayment(double amount) { // implement required method
    System.out.println("Processing $" + amount + " via credit card");
    System.out.println("Card: " + cardNumber);
    // Credit card specific logic here
    return true;                        // simulate successful payment
  }

  @Override
  public String getPaymentType() {      // implement required method
    return "Credit Card";               // return specific type
  }
}

// PayPal implementation
class PayPalProcessor implements PaymentProcessor { // another implementation
  private String email;                 // specific to PayPal

  public PayPalProcessor(String email) {
    this.email = email;                 // set PayPal account
  }

  @Override
  public boolean processPayment(double amount) { // implement required method
    System.out.println("Processing $" + amount + " via PayPal");
    System.out.println("Account: " + email);
    // PayPal specific logic here
    return true;                        // simulate successful payment
  }

  @Override
  public String getPaymentType() {      // implement required method
    return "PayPal";                    // return specific type
  }
}

// Code that uses the interface
class OnlineStore {                     // store class
  // Method works with any payment processor
  public void checkout(PaymentProcessor processor, double amount) {
    System.out.println("Processing order...");
    System.out.println("Payment method: " + processor.getPaymentType());

    if (processor.processPayment(amount)) { // call interface method
      System.out.println("Payment successful!");
    } else {
      System.out.println("Payment failed!");
    }
  }

  public static void main(String[] args) {
    OnlineStore store = new OnlineStore();

    // Different payment methods, same interface
    PaymentProcessor creditCard = new CreditCardProcessor("1234-5678-9012-3456");
    PaymentProcessor paypal = new PayPalProcessor("user@email.com");

    // Same method works with different implementations
    store.checkout(creditCard, 99.99);  // uses credit card processing
    store.checkout(paypal, 149.99);     // uses PayPal processing
  }
}

Summary

These four OOP principles work together to create better programs. Encapsulation protects your data and keeps objects in valid states. Inheritance lets you build specialized classes without starting from scratch. Polymorphism allows your code to work with different types through a common interface. Interfaces define contracts that make your code flexible and testable. Together, these concepts help you write programs that are easier to understand, maintain, and extend. In the next lesson, we'll explore collections and generics, which will help you work with groups of objects efficiently.