1. java
  2. /oop
  3. /inheritance

Master Java Inheritance and Class Hierarchies

Java Inheritance

Inheritance is one of the fundamental principles of object-oriented programming that allows a class to inherit properties and methods from another class. It promotes code reusability, establishes relationships between classes, and enables polymorphism.

What is Inheritance?

Inheritance creates an "is-a" relationship between classes, where a child class (subclass) inherits characteristics from a parent class (superclass). The child class can:

  • Access non-private members of the parent class
  • Override parent methods to provide specific implementations
  • Add its own unique properties and methods
// 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");
    }
}

// Child class (Subclass)
public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age); // Call parent constructor
        this.breed = breed;
    }
    
    public void bark() {
        System.out.println(name + " is barking");
    }
}

// Usage
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
myDog.eat();    // Inherited from Animal
myDog.sleep();  // Inherited from Animal
myDog.bark();   // Specific to Dog

Single Inheritance

Java supports single inheritance, meaning a class can only extend one direct parent class:

public 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...");
    }
}

public class Car extends Vehicle {
    private int doors;
    
    public Car(String brand, int year, int doors) {
        super(brand, year);
        this.doors = doors;
    }
    
    public void honk() {
        System.out.println("Car honking!");
    }
}

// This would cause a compilation error:
// public class SportsCar extends Vehicle, Car { } // NOT ALLOWED

The super Keyword

Calling Parent Constructor

public class Employee {
    protected String name;
    protected double salary;
    
    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

public class Manager extends Employee {
    private String department;
    
    public Manager(String name, double salary, String department) {
        super(name, salary); // Must be first statement
        this.department = department;
    }
}

Accessing Parent Methods

public class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}

public class Circle extends Shape {
    @Override
    public void draw() {
        super.draw(); // Call parent method
        System.out.println("Drawing a circle");
    }
}

// Output:
// Drawing a shape
// Drawing a circle

Accessing Parent Fields

public class Person {
    protected String name = "Unknown";
}

public class Student extends Person {
    private String name = "Student"; // Shadows parent field
    
    public void printNames() {
        System.out.println("Child name: " + this.name);    // "Student"
        System.out.println("Parent name: " + super.name);  // "Unknown"
    }
}

Method Overriding

Override parent methods to provide specific implementations:

public class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound");
    }
    
    public void move() {
        System.out.println("The animal moves");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
    
    @Override
    public void move() {
        System.out.println("The cat prowls silently");
    }
    
    // Cat-specific method
    public void purr() {
        System.out.println("The cat purrs contentedly");
    }
}

public class Bird extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Tweet tweet!");
    }
    
    @Override
    public void move() {
        System.out.println("The bird flies");
    }
}

Rules for Method Overriding

public class Parent {
    // Protected method can be overridden with protected or public
    protected void method1() { }
    
    // Public method can only be overridden with public
    public void method2() { }
    
    // Final methods cannot be overridden
    public final void method3() { }
    
    // Static methods are hidden, not overridden
    public static void staticMethod() {
        System.out.println("Parent static method");
    }
}

public class Child extends Parent {
    @Override
    public void method1() { } // Valid: protected -> public
    
    @Override
    public void method2() { } // Valid: public -> public
    
    // @Override
    // public void method3() { } // ERROR: Cannot override final method
    
    // This hides the parent static method
    public static void staticMethod() {
        System.out.println("Child static method");
    }
}

Abstract Classes

Abstract classes provide a partial implementation that must be extended:

public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Abstract method - must be implemented by subclasses
    public abstract double calculateArea();
    
    // Concrete method - inherited by subclasses
    public void displayInfo() {
        System.out.println("Shape color: " + color);
        System.out.println("Area: " + calculateArea());
    }
}

public class Rectangle extends Shape {
    private double width;
    private double 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 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;
    }
}

// Usage
Shape rectangle = new Rectangle("Red", 5.0, 3.0);
Shape circle = new Circle("Blue", 4.0);

rectangle.displayInfo(); // Uses Rectangle's implementation
circle.displayInfo();    // Uses Circle's implementation

Multilevel Inheritance

Classes can form inheritance chains:

public class LivingBeing {
    protected boolean isAlive = true;
    
    public void breathe() {
        System.out.println("Breathing...");
    }
}

public class Animal extends LivingBeing {
    protected String species;
    
    public Animal(String species) {
        this.species = species;
    }
    
    public void move() {
        System.out.println("Moving...");
    }
}

public class Mammal extends Animal {
    private boolean hasHair = true;
    
    public Mammal(String species) {
        super(species);
    }
    
    public void produceMilk() {
        System.out.println("Producing milk...");
    }
}

public class Dog extends Mammal {
    private String breed;
    
    public Dog(String breed) {
        super("Canine");
        this.breed = breed;
    }
    
    public void bark() {
        System.out.println("Woof!");
    }
}

// Dog inherits from all levels
Dog myDog = new Dog("Labrador");
myDog.breathe();      // From LivingBeing
myDog.move();         // From Animal
myDog.produceMilk();  // From Mammal
myDog.bark();         // From Dog

Real-World Examples

1. Employee Hierarchy

public abstract class Employee {
    protected String name;
    protected String employeeId;
    protected double baseSalary;
    
    public Employee(String name, String employeeId, double baseSalary) {
        this.name = name;
        this.employeeId = employeeId;
        this.baseSalary = baseSalary;
    }
    
    public abstract double calculateSalary();
    
    public void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("ID: " + employeeId);
        System.out.println("Salary: $" + calculateSalary());
    }
}

public class FullTimeEmployee extends Employee {
    private double benefits;
    
    public FullTimeEmployee(String name, String id, double baseSalary, double benefits) {
        super(name, id, baseSalary);
        this.benefits = benefits;
    }
    
    @Override
    public double calculateSalary() {
        return baseSalary + benefits;
    }
}

public class PartTimeEmployee extends Employee {
    private int hoursWorked;
    private double hourlyRate;
    
    public PartTimeEmployee(String name, String id, int hours, double rate) {
        super(name, id, 0); // No base salary
        this.hoursWorked = hours;
        this.hourlyRate = rate;
    }
    
    @Override
    public double calculateSalary() {
        return hoursWorked * hourlyRate;
    }
}

public class Contractor extends Employee {
    private double contractAmount;
    
    public Contractor(String name, String id, double contractAmount) {
        super(name, id, 0);
        this.contractAmount = contractAmount;
    }
    
    @Override
    public double calculateSalary() {
        return contractAmount;
    }
}

2. Vehicle System

public abstract class Vehicle {
    protected String make;
    protected String model;
    protected int year;
    protected double fuelLevel;
    
    public Vehicle(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.fuelLevel = 100.0; // Start with full tank
    }
    
    public abstract void start();
    public abstract double getFuelEfficiency();
    
    public void refuel() {
        this.fuelLevel = 100.0;
        System.out.println("Vehicle refueled");
    }
    
    public double calculateRange() {
        return fuelLevel * getFuelEfficiency();
    }
}

public class Car extends Vehicle {
    private int numberOfDoors;
    
    public Car(String make, String model, int year, int doors) {
        super(make, model, year);
        this.numberOfDoors = doors;
    }
    
    @Override
    public void start() {
        System.out.println("Car engine starting with key ignition");
    }
    
    @Override
    public double getFuelEfficiency() {
        return 25.0; // 25 miles per gallon
    }
}

public class Motorcycle extends Vehicle {
    private int engineSize;
    
    public Motorcycle(String make, String model, int year, int engineSize) {
        super(make, model, year);
        this.engineSize = engineSize;
    }
    
    @Override
    public void start() {
        System.out.println("Motorcycle starting with kick start");
    }
    
    @Override
    public double getFuelEfficiency() {
        return 45.0; // 45 miles per gallon
    }
}

public class ElectricCar extends Car {
    private double batteryCapacity;
    
    public ElectricCar(String make, String model, int year, int doors, double battery) {
        super(make, model, year, doors);
        this.batteryCapacity = battery;
    }
    
    @Override
    public void start() {
        System.out.println("Electric car starting silently");
    }
    
    @Override
    public double getFuelEfficiency() {
        return 100.0; // 100 miles per charge
    }
    
    @Override
    public void refuel() {
        System.out.println("Charging electric vehicle");
        super.refuel(); // Reuse parent logic
    }
}

Best Practices

1. Favor Composition Over Inheritance

Sometimes composition is better than inheritance:

// Instead of inheritance
public class Bird extends Animal {
    public void fly() { /* flying logic */ }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

// Consider composition
public class Animal {
    private MovementBehavior movementBehavior;
    
    public Animal(MovementBehavior behavior) {
        this.movementBehavior = behavior;
    }
    
    public void move() {
        movementBehavior.move();
    }
}

interface MovementBehavior {
    void move();
}

class FlyingBehavior implements MovementBehavior {
    public void move() { System.out.println("Flying"); }
}

class SwimmingBehavior implements MovementBehavior {
    public void move() { System.out.println("Swimming"); }
}

2. Use @Override Annotation

Always use @Override when overriding methods:

public class Parent {
    public void method() { }
}

public class Child extends Parent {
    @Override // Helps catch errors at compile time
    public void method() { }
    
    // @Override // This will cause compilation error
    // public void methd() { } // Typo in method name
}

3. Design for Inheritance or Prohibit It

// Design for inheritance - document and provide hooks
public class ExtendableClass {
    /**
     * Hook for subclasses to customize behavior.
     * Called by process() method.
     */
    protected void beforeProcess() {
        // Default implementation - can be overridden
    }
    
    public final void process() {
        beforeProcess();
        // Core processing logic
        afterProcess();
    }
    
    protected void afterProcess() {
        // Default implementation - can be overridden
    }
}

// Prohibit inheritance with final
public final class UtilityClass {
    private UtilityClass() { } // Prevent instantiation
    
    public static void utilityMethod() {
        // Utility logic
    }
}

Common Pitfalls

1. Calling Overridable Methods in Constructor

public class Parent {
    public Parent() {
        init(); // Dangerous - calls overridable method
    }
    
    protected void init() {
        System.out.println("Parent init");
    }
}

public class Child extends Parent {
    private String value = "initialized";
    
    @Override
    protected void init() {
        System.out.println("Child init: " + value); // value might be null!
    }
}

2. Breaking Liskov Substitution Principle

public class Rectangle {
    protected int width, height;
    
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

// This violates LSP - Square is not substitutable for Rectangle
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width; // Changes both dimensions
    }
    
    @Override
    public void setHeight(int height) {
        this.width = this.height = height; // Changes both dimensions
    }
}

Summary

Java inheritance enables:

  • Code Reusability: Share common functionality across classes
  • Polymorphism: Treat objects of different types uniformly
  • Abstraction: Define common interfaces through abstract classes
  • Extensibility: Build upon existing functionality

Key concepts:

  • Single inheritance with extends keyword
  • Method overriding with @Override annotation
  • super keyword for accessing parent class members
  • Abstract classes for partial implementations
  • Design considerations for maintainable inheritance hierarchies

Use inheritance judiciously - favor composition when the relationship isn't truly "is-a" and always design for extensibility or explicitly prevent it.