Course Content

Inheritance in Java: Building Hierarchies and Reusing Code

Introduction: The IS-A Relationship

Imagine a family tree. A child inherits traits from parents, but also develops unique characteristics of their own. This natural concept is mirrored in programming through inheritance—one of the four fundamental pillars of Object-Oriented Programming.

Inheritance allows a new class (child/subclass) to acquire properties and behaviors from an existing class (parent/superclass). It establishes an "IS-A" relationship, enabling code reuse and hierarchical classification.

The Basics: How Inheritance Works

Simple Inheritance Example

java
// Parent/Super Class
public class Vehicle {
    private String brand;
    private String model;
    private int year;
    
    public Vehicle(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }
    
    public void start() {
        System.out.println(brand + " " + model + " is starting...");
    }
    
    public void stop() {
        System.out.println(brand + " " + model + " is stopping...");
    }
    
    public void displayInfo() {
        System.out.println("Brand: " + brand + 
                         ", Model: " + model + 
                         ", Year: " + year);
    }
    
    // Getters and setters
    public String getBrand() { return brand; }
    public String getModel() { return model; }
    public int getYear() { return year; }
}

// Child/Sub Class
public class Car extends Vehicle {  // Car IS-A Vehicle
    private int numberOfDoors;
    private String fuelType;
    
    public Car(String brand, String model, int year, 
               int doors, String fuelType) {
        super(brand, model, year);  // Call parent constructor
        this.numberOfDoors = doors;
        this.fuelType = fuelType;
    }
    
    // Additional method specific to Car
    public void honk() {
        System.out.println(getBrand() + " " + getModel() + " is honking!");
    }
    
    // Overriding parent method
    @Override
    public void displayInfo() {
        super.displayInfo();  // Call parent method
        System.out.println("Doors: " + numberOfDoors + 
                         ", Fuel: " + fuelType);
    }
}

Using the Classes

java
public class InheritanceDemo {
    public static void main(String[] args) {
        // Create a Vehicle
        Vehicle vehicle = new Vehicle("Generic", "Transport", 2020);
        vehicle.start();      // Generic Transport is starting...
        vehicle.displayInfo();// Brand: Generic, Model: Transport, Year: 2020
        
        // Create a Car (which inherits from Vehicle)
        Car car = new Car("Toyota", "Camry", 2023, 4, "Petrol");
        car.start();          // Inherited from Vehicle
        car.honk();           // Unique to Car
        car.displayInfo();    // Overridden method
        
        // Output of car.displayInfo():
        // Brand: Toyota, Model: Camry, Year: 2023
        // Doors: 4, Fuel: Petrol
    }
}

Types of Inheritance

1. Single Inheritance (One Parent → One Child)

java
// Parent Class
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");
    }
}

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

2. Multilevel Inheritance (Chain of Inheritance)

java
// Grandparent Class
public class Animal {
    public void breathe() {
        System.out.println("Breathing...");
    }
}

// Parent Class
public class Mammal extends Animal {
    public void feedMilk() {
        System.out.println("Feeding milk to young ones");
    }
}

// Child Class
public class Dog extends Mammal {  // Inherits from Mammal, which inherits from Animal
    public void bark() {
        System.out.println("Barking...");
    }
}

// Usage
public class MultilevelDemo {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.breathe();    // From Animal (grandparent)
        dog.feedMilk();   // From Mammal (parent)
        dog.bark();       // Its own method
    }
}

3. Hierarchical Inheritance (One Parent → Multiple Children)

java
// Parent Class
public class Employee {
    protected String name;
    protected double salary;
    
    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    
    public void work() {
        System.out.println(name + " is working");
    }
}

// Child Class 1
public class Developer extends Employee {
    private String programmingLanguage;
    
    public Developer(String name, double salary, String language) {
        super(name, salary);
        this.programmingLanguage = language;
    }
    
    public void code() {
        System.out.println(name + " is coding in " + programmingLanguage);
    }
}

// Child Class 2
public class Manager extends Employee {
    private int teamSize;
    
    public Manager(String name, double salary, int teamSize) {
        super(name, salary);
        this.teamSize = teamSize;
    }
    
    public void conductMeeting() {
        System.out.println(name + " is conducting a meeting with " + 
                         teamSize + " team members");
    }
}

// Usage
public class HierarchicalDemo {
    public static void main(String[] args) {
        Developer dev = new Developer("Alice", 80000, "Java");
        Manager mgr = new Manager("Bob", 100000, 5);
        
        dev.work();      // Inherited from Employee
        dev.code();      // Unique to Developer
        
        mgr.work();      // Inherited from Employee
        mgr.conductMeeting(); // Unique to Manager
    }
}

4. Hybrid Inheritance (Mix of Multiple Types)

Note: Java doesn't support multiple inheritance with classes (to avoid the diamond problem), but it can be achieved through interfaces.

The super Keyword: Accessing Parent Members

The super keyword is used to refer to the immediate parent class.

1. Using super to Call Parent Constructor

java
public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Student extends Person {
    private int studentId;
    private String major;
    
    public Student(String name, int age, int studentId, String major) {
        super(name, age);  // MUST be first statement in constructor
        this.studentId = studentId;
        this.major = major;
    }
}

2. Using super to Call Parent Methods

java
public class Smartphone {
    public void makeCall(String number) {
        System.out.println("Calling " + number + "...");
    }
}

public class SmartphonePro extends Smartphone {
    @Override
    public void makeCall(String number) {
        System.out.println("Checking call quality...");
        super.makeCall(number);  // Call parent's makeCall method
        System.out.println("Recording call...");
    }
}

3. Using super to Access Parent Variables

java
public class Parent {
    protected String message = "Hello from Parent";
}

public class Child extends Parent {
    private String message = "Hello from Child";
    
    public void displayMessages() {
        System.out.println("Child message: " + message);
        System.out.println("Parent message: " + super.message);
    }
}

Method Overriding: Redefining Parent Behavior

Method overriding allows a subclass to provide a specific implementation of a method already defined in its parent class.

Rules for Method Overriding:

  1. Method name must be the same

  2. Parameter list must be the same

  3. Return type must be the same or covariant (subtype)

  4. Access modifier cannot be more restrictive

  5. Cannot override finalprivate, or static methods

java
public class BankAccount {
    protected double balance;
    
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }
    
    public void deposit(double amount) {
        balance += amount;
        System.out.println("Deposited: $" + amount);
    }
    
    public void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
        } else {
            System.out.println("Insufficient funds!");
        }
    }
    
    public double calculateInterest() {
        return balance * 0.02;  // 2% interest for general accounts
    }
}

public class SavingsAccount extends BankAccount {
    private double interestRate;
    
    public SavingsAccount(double initialBalance, double interestRate) {
        super(initialBalance);
        this.interestRate = interestRate;
    }
    
    // Override calculateInterest method
    @Override
    public double calculateInterest() {
        return balance * interestRate;  // Use specific interest rate
    }
    
    // Override withdraw with additional restriction
    @Override
    public void withdraw(double amount) {
        if (amount <= balance && amount <= 5000) {  // Max withdrawal limit
            super.withdraw(amount);
        } else if (amount > 5000) {
            System.out.println("Withdrawal limit exceeded! Max: $5000");
        } else {
            System.out.println("Insufficient funds!");
        }
    }
    
    // Additional method specific to SavingsAccount
    public void applyInterest() {
        double interest = calculateInterest();
        balance += interest;
        System.out.println("Interest applied: $" + interest);
    }
}

Using @Override Annotation

Always use the @Override annotation—it helps catch errors at compile time:

java
public class Child extends Parent {
    @Override  // Compiler will check if this actually overrides a parent method
    public void display() {
        System.out.println("Child's display");
    }
}

Access Modifiers in Inheritance

Understanding how access modifiers work with inheritance is crucial:

 
 
Modifier Same Class Same Package Subclass World
private
default
protected
public
java
public class Parent {
    private String privateVar = "Private";      // Not accessible in child
    String defaultVar = "Default";             // Accessible in same package only
    protected String protectedVar = "Protected"; // Accessible in child
    public String publicVar = "Public";        // Accessible everywhere
}

public class Child extends Parent {
    public void display() {
        // System.out.println(privateVar);    // ERROR: private
        // System.out.println(defaultVar);    // ERROR if in different package
        System.out.println(protectedVar);      // OK
        System.out.println(publicVar);         // OK
    }
}

Constructor Chaining in Inheritance

When creating an object of a subclass, constructors are called in order from parent to child.

java
public class A {
    public A() {
        System.out.println("Constructor A");
    }
}

public class B extends A {
    public B() {
        // super(); // Implicitly called by compiler
        System.out.println("Constructor B");
    }
}

public class C extends B {
    public C() {
        // super(); // Implicitly called by compiler
        System.out.println("Constructor C");
    }
}

public class ConstructorChainDemo {
    public static void main(String[] args) {
        C obj = new C();
        // Output:
        // Constructor A
        // Constructor B
        // Constructor C
    }
}

The final Keyword with Inheritance

The final keyword prevents inheritance or method overriding:

java
// final class cannot be extended
final public class President {
    // final method cannot be overridden
    public final void takeOath() {
        System.out.println("I swear to uphold the constitution");
    }
}

// This would cause compilation error:
// public class VicePresident extends President { } // ERROR: Cannot inherit from final class

Abstract Classes and Inheritance

Abstract classes are designed to be inherited. They can't be instantiated directly.

java
// Abstract class (incomplete class)
abstract public class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Abstract method (no implementation)
    abstract public double calculateArea();
    
    // Concrete method
    public void display() {
        System.out.println("Color: " + color);
        System.out.println("Area: " + calculateArea());
    }
}

// Concrete class must implement abstract methods
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;
    }
}

// Another concrete class
public class Rectangle extends Shape {
    private double length;
    private double width;
    
    public Rectangle(String color, double length, double width) {
        super(color);
        this.length = length;
        this.width = width;
    }
    
    @Override
    public double calculateArea() {
        return length * width;
    }
}

Real-World Example: E-Learning System

java
import java.util.ArrayList;
import java.util.Date;

// Base class
abstract public class Content {
    protected String title;
    protected String author;
    protected Date uploadDate;
    protected int viewCount;
    
    public Content(String title, String author) {
        this.title = title;
        this.author = author;
        this.uploadDate = new Date();
        this.viewCount = 0;
    }
    
    public void view() {
        viewCount++;
        System.out.println("Viewing: " + title);
    }
    
    abstract public void displayDetails();
    
    // Getters
    public String getTitle() { return title; }
    public int getViewCount() { return viewCount; }
}

// Derived class 1
public class Video extends Content {
    private int duration; // in minutes
    private String resolution;
    
    public Video(String title, String author, int duration, String resolution) {
        super(title, author);
        this.duration = duration;
        this.resolution = resolution;
    }
    
    @Override
    public void displayDetails() {
        System.out.println("VIDEO: " + title);
        System.out.println("Author: " + author);
        System.out.println("Duration: " + duration + " minutes");
        System.out.println("Resolution: " + resolution);
        System.out.println("Views: " + viewCount);
    }
    
    public void play() {
        view();
        System.out.println("Playing video: " + title);
    }
}

// Derived class 2
public class Article extends Content {
    private String content;
    private int wordCount;
    
    public Article(String title, String author, String content) {
        super(title, author);
        this.content = content;
        this.wordCount = content.split("\\s+").length;
    }
    
    @Override
    public void displayDetails() {
        System.out.println("ARTICLE: " + title);
        System.out.println("Author: " + author);
        System.out.println("Word Count: " + wordCount);
        System.out.println("Views: " + viewCount);
    }
    
    public void read() {
        view();
        System.out.println("Reading article: " + title);
        System.out.println(content.substring(0, Math.min(100, content.length())) + "...");
    }
}

// Derived class 3 (Multi-level inheritance)
public class PremiumVideo extends Video {
    private boolean downloadable;
    private ArrayList<String> subtitles;
    
    public PremiumVideo(String title, String author, int duration, 
                        String resolution, boolean downloadable) {
        super(title, author, duration, resolution);
        this.downloadable = downloadable;
        this.subtitles = new ArrayList<>();
    }
    
    public void addSubtitle(String language) {
        subtitles.add(language);
        System.out.println("Added " + language + " subtitles");
    }
    
    @Override
    public void displayDetails() {
        super.displayDetails();
        System.out.println("Downloadable: " + downloadable);
        System.out.println("Available subtitles: " + subtitles);
    }
}

// Main application
public class ELearningSystem {
    public static void main(String[] args) {
        // Polymorphic array
        Content[] library = new Content[3];
        
        library[0] = new Video("Java Basics", "John Doe", 45, "1080p");
        library[1] = new Article("OOP Principles", "Jane Smith", 
                                "Object-oriented programming is a paradigm...");
        library[2] = new PremiumVideo("Advanced Java", "Alice Johnson", 
                                     120, "4K", true);
        
        // Process all content polymorphically
        for (Content item : library) {
            item.view();
            item.displayDetails();
            System.out.println("--------------------");
            
            // Type-specific operations
            if (item instanceof Video) {
                ((Video) item).play();
            } else if (item instanceof Article) {
                ((Article) item).read();
            }
            System.out.println("====================");
        }
    }
}

Composition vs Inheritance

Sometimes composition ("HAS-A" relationship) is better than inheritance ("IS-A"):

java
// Using Inheritance (not always the best choice)
public class Car extends Engine {  // A Car IS-NOT an Engine
    // Problematic design
}

// Using Composition (better design)
public class Engine {
    private String type;
    private int horsepower;
    
    public void start() {
        System.out.println("Engine started");
    }
}

public class Car {
    private Engine engine;  // Car HAS-A Engine
    private String model;
    
    public Car(String model, String engineType, int horsepower) {
        this.model = model;
        this.engine = new Engine(engineType, horsepower);
    }
    
    public void startCar() {
        engine.start();
        System.out.println(model + " is ready to go!");
    }
}

When to use Inheritance:

  • True "IS-A" relationship exists

  • You need polymorphism

  • You want to extend framework classes

  • Code reuse is significant

When to use Composition:

  • "HAS-A" relationship exists

  • You need flexibility to change behavior at runtime

  • You want to avoid deep inheritance hierarchies

  • You need to reuse code from multiple sources

Common Pitfalls and Best Practices

Pitfall 1: Improper Use of Inheritance

java
// ❌ BAD: Square extends Rectangle (famous Liskov Substitution Principle violation)
public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w); // Violates rectangle behavior
    }
    
    @Override
    public void setHeight(int h) {
        super.setWidth(h);
        super.setHeight(h); // Violates rectangle behavior
    }
}

Pitfall 2: Deep Inheritance Hierarchies

java
// ❌ BAD: Too deep hierarchy
class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
class GuardDog extends Dog {}
class TrainedGuardDog extends GuardDog {}
class PoliceDog extends TrainedGuardDog {}
// Too many levels - hard to maintain!

// ✅ BETTER: Use composition
class Dog {
    private Training training;
    private Role role;
}

class Training {
    private String type;
    private int level;
}

class Role {
    private String description;
}

Best Practices:

  1. Follow Liskov Substitution Principle: Subclasses should be substitutable for their base classes

  2. Favor composition over inheritance when possible

  3. Keep inheritance hierarchies shallow (2-3 levels max)

  4. Make superclasses abstract when they shouldn't be instantiated

  5. Use @Override annotation consistently

  6. Don't violate encapsulation in parent classes

  7. Use protected access carefully - it breaks encapsulation

  8. Consider using interfaces for multiple inheritance needs

Liskov Substitution Principle Example

java
// Good example: All birds can eat
abstract class Bird {
    public abstract void eat();
}

// Flying birds can also fly
abstract class FlyingBird extends Bird {
    public abstract void fly();
}

// Non-flying birds can't fly
abstract class NonFlyingBird extends Bird {
    // No fly method here
}

class Sparrow extends FlyingBird {
    @Override
    public void eat() { System.out.println("Eating seeds"); }
    
    @Override
    public void fly() { System.out.println("Flying high"); }
}

class Penguin extends NonFlyingBird {
    @Override
    public void eat() { System.out.println("Eating fish"); }
    // No fly method - correct!
}

Advanced Topic: Covariant Return Types

Java allows overriding a method with a return type that is a subclass of the original return type.

java
class Animal {
    public Animal reproduce() {
        System.out.println("Animal reproducing");
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    public Dog reproduce() {  // Covariant return type
        System.out.println("Dog reproducing");
        return new Dog();
    }
}

public class CovariantDemo {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal offspring1 = animal.reproduce();  // Returns Animal
        
        Dog dog = new Dog();
        Dog offspring2 = dog.reproduce();  // Returns Dog (covariant)
    }
}

Conclusion

Inheritance is a powerful mechanism in Java that enables:

  • Code reuse - avoiding duplication

  • Polymorphism - treating objects of different types uniformly

  • Hierarchical organization - modeling real-world relationships

  • Extensibility - adding new functionality without modifying existing code

Remember the key principles:

  1. Inheritance establishes an "IS-A" relationship

  2. Use extends keyword for class inheritance

  3. Use super to access parent members

  4. Method overriding enables polymorphism

  5. Constructors chain from parent to child

  6. Abstract classes define contracts for subclasses

 
Course Reviews

No reviews yet.