Java Tutorial » Chapter 23 — Object-Oriented Programming

Chapter 23 — Java Object-Oriented Programming

Mastering the core principles of OOP: inheritance, polymorphism, and relationships.

1. Introduction to OOP

What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data in the form of attributes (properties) and code in the form of methods (behaviors). The core principles of OOP are:

  • Encapsulation: Bundling data (attributes) and methods (behaviors) within objects.
  • Inheritance: Creating new classes based on existing ones, inheriting their attributes and methods.
  • Polymorphism: Using a single interface to represent different underlying forms (data types).
  • Abstraction: Hiding complex implementation details behind a simple interface.

In Java, OOP is implemented through classes and interfaces, with inheritance being the primary mechanism for code reuse and polymorphism.

2. The extends Keyword

Inheriting from a Parent Class

The extends keyword is used to create a subclass (or child class) that inherits from a superclass (or parent class). The subclass inherits all non-private attributes and methods from the superclass.

Inheritance creates an "IS-A" relationship between the subclass and superclass. For example, a "Car IS-A Vehicle". This relationship is fundamental to OOP and enables code reuse.

// Superclass
class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
        System.out.println("Vehicle created: " + brand + " " + year);
    }
    
    public void start() {
        System.out.println("Vehicle starting...");
    }
    
    public void displayInfo() {
        System.out.println("Brand: " + brand + ", Year: " + year);
    }
}

// Subclass that extends Vehicle
class Car extends Vehicle {
    private String model;
    private boolean isEngineOn;

    public Car(String brand, int year, String model) {
        // Call to superclass constructor using super()
        super(brand, year);
        this.model = model;
        this.isEngineOn = false;
        System.out.println("Car created: " + brand + " " + year + " " + model);
    }
    
    @Override
    public void start() {
        // Call to parent method
        super.start();
        System.out.println(model + " engine started");
        isEngineOn = true;
    }
    
    public void turnOffEngine() {
        isEngineOn = false;
        System.out.println(model + " engine turned off");
    }
    
    @Override
    public void displayInfo() {
        // Call to parent method and add subclass-specific information
        super.displayInfo();
        System.out.println("Model: " + model);
        System.out.println("Engine Status: " + (isEngineOn ? "On" : "Off"));
    }
}
3. Sample Code

Complete Example

Here's a complete example that demonstrates inheritance, method overriding, and the use of super to call a parent constructor:

public class InheritanceExample {
    public static void main(String[] args) {
        Car myCar = new Car("Toyota", 2022, "Camry");
        myCar.start();
        myCar.displayInfo();
        myCar.turnOffEngine();
    }
}
4. The super Keyword

Accessing Parent Class Members

The super keyword has two main uses in Java:

  1. Calling a Superclass Constructor: super(args...) calls the constructor of the parent class. This must be the first statement in a subclass constructor.
  2. Accessing Superclass Methods: super.methodName(args...) calls a method from the parent class, even if it's overridden in the subclass.
  3. Accessing Superclass Fields: super.fieldName accesses a field from the parent class (must be protected or in the same package).
class ElectricCar extends Car {
    private int batteryCapacity;

    public ElectricCar(String brand, int year, String model, int batteryCapacity) {
        // Call to parent constructor
        super(brand, year, model);
        this.batteryCapacity = batteryCapacity;
        System.out.println("Electric car created with battery capacity: " + batteryCapacity + " kWh");
    }
    
    @Override
    public void start() {
        // Call to parent method
        super.start();
        System.out.println("Electric system engaged with " + batteryCapacity + " kWh battery.");
    }
    
    @Override
    public void displayInfo() {
        // Call to parent method and add subclass-specific information
        super.displayInfo();
        System.out.println("Battery Capacity: " + batteryCapacity + " kWh");
    }
}
5. IS-A Relationship

The Core of Inheritance

The "IS-A" relationship is the foundation of inheritance. When we say "A Car IS-A Vehicle", we're stating that a Car is a specialized type of Vehicle. This relationship allows us to:

  • Use a subclass object wherever a superclass object is expected (polymorphism).
  • Reuse code by inheriting from existing classes.
  • Create more specialized classes by extending general ones.

For example, a method that accepts a Vehicle parameter can also accept a Car object, because a Car IS-A Vehicle.

public class VehicleService {
    public void performMaintenance(Vehicle vehicle) {
        System.out.println("Performing maintenance on: " + vehicle.getClass().getSimpleName());
        
        // Polymorphic behavior - different vehicles require different maintenance
        if (vehicle instanceof Car) {
            System.out.println("Changing oil for the car");
        } else if (vehicle instanceof Motorcycle) {
            System.out.println("Checking chain for the motorcycle");
        } else {
            System.out.println("General maintenance for the vehicle");
        }
    }
}
6. HAS-A Relationship

Composition Over Inheritance

The "HAS-A" relationship represents composition, where an object contains other objects as parts. For example, a Car HAS-A Engine. This is different from inheritance and is typically preferred for flexible design.

class Engine {
    private int horsepower;
    private String type;
    
    public Engine(int horsepower, String type) {
        this.horsepower = horsepower;
        this.type = type;
    }
    
    public void start() {
        System.out.println(type + " engine with " + horsepower + " HP starting...");
    }
    
    public String getDetails() {
        return type + " engine, " + horsepower + " HP";
    }
}

class Car {
    private Engine engine; // Car HAS-A Engine
    private String model;
    private int year;
    
    public Car(String model, int year, Engine engine) {
        this.model = model;
        this.year = year;
        this.engine = engine; // Composition: Car contains an Engine
        System.out.println("Car created with model: " + model + ", year: " + year);
        System.out.println("Engine: " + engine.getDetails());
    }
    
    public void startCar() {
        engine.start();
        System.out.println(model + " started");
    }
}
7. The instanceof Keyword

Checking Object Types

The instanceof operator checks if an object is an instance of a specific class or subclass. It returns a boolean value and is often used before casting to ensure the operation is safe.

public class InstanceofExample {
    public static void processVehicle(Vehicle vehicle) {
        // Safe downcasting with instanceof
        if (vehicle instanceof Car) {
            Car car = (Car) vehicle;
            System.out.println("Processing a Car: " + car);
            // Can safely call Car-specific methods
            if (car instanceof ElectricCar) {
                ElectricCar electricCar = (ElectricCar) car;
                System.out.println("Battery capacity: " + electricCar.getBatteryCapacity());
            }
        } else if (vehicle instanceof Motorcycle) {
            System.out.println("Processing a Motorcycle");
        } else {
            System.out.println("Processing an unknown vehicle");
        }
    }
}
8. Types of Inheritance

Class Hierarchies

Java supports different types of inheritance based on how classes are related:

  • Single Inheritance: A class extends one superclass. This is the most common form.
  • Multiple Inheritance: A class extends multiple superclasses. Java doesn't support this directly, but it can be simulated using interfaces.
  • Multilevel Inheritance: A class extends another class, which in turn extends another class, creating a chain.
  • Hybrid Inheritance: Combining inheritance with composition to get benefits of both.

Single Inheritance Example

// Superclass
class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
    
    public void start() {
        System.out.println("Vehicle starting...");
    }
}

// Subclass that extends Vehicle
class Car extends Vehicle {
    private String model;
    
    public Car(String brand, int year, String model) {
        super(brand, year);
        this.model = model;
    }
    
    @Override
    public void start() {
        super.start();
        System.out.println(model + " starting...");
    }
}

Multilevel Inheritance Example

// Top-level class
class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

// Middle-level class
class Mammal extends Animal {
    protected boolean warmBlooded;
    
    public Mammal(String name, boolean warmBlooded) {
        super(name);
        this.warmBlooded = warmBlooded;
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " makes a mammal sound");
    }
}

// Bottom-level class
class Dog extends Mammal {
    public Dog(String name) {
        super(name, true); // Dogs are warm-blooded
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + " barks");
    }
}

Multiple Inheritance Simulation with Interfaces

// Multiple inheritance simulation using interfaces
interface Drivable {
    void drive();
}

class Vehicle implements Drivable {
    public void drive() {
        System.out.println("Driving a vehicle");
    }
}

class Electric implements Drivable {
    public void drive() {
        System.out.println("Driving an electric vehicle");
    }
}

// Hybrid class using composition and interfaces
class HybridCar {
    private Vehicle vehicle;
    private Electric electric;
    
    public HybridCar(Vehicle vehicle, Electric electric) {
        this.vehicle = vehicle;
        this.electric = electric;
    }
    
    public void driveInGasMode() {
        vehicle.drive();
    }
    
    public void driveInElectricMode() {
        electric.drive();
    }
}
9. Advanced OOP Concepts

Beyond the Basics

Let's explore some advanced OOP concepts that build on the foundation of inheritance and polymorphism:

Abstract Classes and Methods

An abstract class cannot be instantiated and may contain abstract methods that must be implemented by subclasses. This is a powerful tool for defining common interfaces.

// Abstract class defining a common interface
public abstract class Shape {
    protected String name;
    
    public Shape(String name) {
        this.name = name;
    }
    
    // Abstract method that must be implemented by subclasses
    public abstract double calculateArea();
    
    // Concrete method that all shapes can use
    public void displayInfo() {
        System.out.println("Shape: " + name + ", Area: " + calculateArea());
    }
}

// Concrete implementation
class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        super("Circle");
        this.radius = radius;
    }
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        super("Rectangle");
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double calculateArea() {
        return width * height;
    }
}

Final Classes and Methods

A final class cannot be subclassed. Its methods cannot be overridden. This is useful for creating immutable objects or for security purposes.

// Final class that cannot be extended
public final class ImmutablePerson {
    private final String name;
    private final int age;
    
    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Only getters, no setters to maintain immutability
    public String getName() { return name; }
    public int getAge() { return age; }
    
    // No setters that would allow modification
}

The Object Class and Key Methods

Every class in Java implicitly inherits from the Object class. Understanding its key methods is crucial for effective Java programming:

Method Description When to Override
toString() Returns a string representation of the object. Always override for meaningful output. Always, especially for data objects
equals(Object obj) Compares this object to another for equality. Must override for proper comparison. Always, especially for collections and maps
hashCode() Returns a hash code for the object. Override when using in hash-based collections. When overriding equals()
clone() Creates and returns a copy of the object. Must implement Cloneable interface. When object is mutable and needs to be copied
10. Practice & Challenge

Test Your Skills

  1. Create a class hierarchy for different types of bank accounts, with a base Account class and subclasses like SavingsAccount and CheckingAccount.
  2. Implement a method that processes different account types polymorphically.
  3. Create a class that demonstrates the "HAS-A" relationship, like a Computer that has a Processor.
  4. Use instanceof to determine the specific type of an account before performing operations.

🏆 Challenge: Employee Management System

Create a simple employee management system with the following requirements:

  • Create an abstract Employee base class with properties like name, ID, and salary.
  • Create concrete subclasses like FullTimeEmployee and PartTimeEmployee that extend Employee.
  • Implement a method that calculates the total payroll for all employees, demonstrating polymorphism.
  • Use instanceof to differentiate between employee types when needed.

abstract class Employee {
    protected String name;
    protected int id;
    protected double salary;

    public Employee(String name, int id, double salary) {
        this.name = name;
        this.id = id;
        this.salary = salary;
    }
    
    public abstract double calculateMonthlyPay();
    public String getDetails() {
        return "ID: " + id + ", Name: " + name;
    }
}

class FullTimeEmployee extends Employee {
    public FullTimeEmployee(String name, int id, double salary) {
        super(name, id, salary);
    }
    
    @Override
    public double calculateMonthlyPay() {
        return salary; // Full-time employees get their full salary
    }
    
    @Override
    public String getDetails() {
        return super.getDetails() + ", Type: Full-time";
    }
}

class PartTimeEmployee extends Employee {
    private double hourlyRate;
    private int hoursWorked;

    public PartTimeEmployee(String name, int id, double hourlyRate) {
        super(name, id, 0); // Salary is calculated per pay period
        this.hourlyRate = hourlyRate;
    }
    
    public void setHoursWorked(int hours) {
        this.hoursWorked = hours;
    }
    
    @Override
    public double calculateMonthlyPay() {
        return hourlyRate * hoursWorked * 4; // Assuming 4 weeks in a month
    }
    
    @Override
    public String getDetails() {
        return super.getDetails() + ", Type: Part-time";
    }
}

public class PayrollSystem {
    public static void main(String[] args) {
        Employee[] employees = {
            new FullTimeEmployee("Alice", 1, 5000),
            new PartTimeEmployee("Bob", 2, 20),
            new FullTimeEmployee("Charlie", 3, 6000)
        };
        
        double totalPayroll = 0;
        for (Employee emp : employees) {
            System.out.println(emp.getDetails());
            totalPayroll += emp.calculateMonthlyPay();
            
            // Using instanceof to determine type
            if (emp instanceof FullTimeEmployee) {
                System.out.println("Full-time employee");
            } else if (emp instanceof PartTimeEmployee) {
                System.out.println("Part-time employee");
            }
        }
        
        System.out.printf("Total monthly payroll: $%.2f%n", totalPayroll);
    }
}