Index

Inheritance

Java Object-Oriented Design

4.1 extends Keyword and Inheriting Methods

Inheritance is a foundational concept in object-oriented programming that enables one class to acquire properties and behaviors of another class. In Java, this is accomplished using the extends keyword.

What Is the extends Keyword?

The extends keyword is used to declare that a new class (called a subclass or child class) is derived from an existing class (called a superclass or parent class). This establishes an is-a relationship, meaning the subclass is a specialized type of the superclass.

Syntax:

class SubClass extends SuperClass {
    // additional fields and methods
}

For example:

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Barking...");
    }
}

Here, Dog extends Animal, meaning Dog inherits all accessible members (fields and methods) from Animal.

What Does a Subclass Inherit?

When a class extends another:

In the Animal and Dog example, Dog inherits the eat() method and can call it directly:

Dog d = new Dog();
d.eat();  // Output: Eating...
d.bark(); // Output: Barking...

Simple Class Hierarchy Example

Consider a class hierarchy representing vehicles:

class Vehicle {
    String brand;

    void start() {
        System.out.println("Vehicle is starting");
    }
}

class Car extends Vehicle {
    int numberOfDoors;

    void honk() {
        System.out.println("Car horn sounds");
    }
}

Here, Car inherits the brand field and the start() method from Vehicle. It also adds its own field numberOfDoors and method honk().

Usage:

Car myCar = new Car();
myCar.brand = "Toyota";  // inherited field
myCar.start();           // inherited method
myCar.honk();            // subclass method

What Is Not Inherited?

For example:

class Person {
    private String ssn;  // private, not inherited

    public String getSsn() {
        return ssn;
    }
}

class Employee extends Person {
    void printSsn() {
        // System.out.println(ssn); // Error: ssn is private in Person
        System.out.println(getSsn()); // Allowed via public getter
    }
}
Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();   // Inherited method
        d.bark();  // Dog's own method

        Car myCar = new Car();
        myCar.brand = "Toyota";   // inherited field
        myCar.start();            // inherited method
        myCar.honk();             // Car's own method

        Employee emp = new Employee();
        emp.setSsn("123-45-6789");
        emp.printSsn();           // prints SSN via getter
    }
}

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Barking...");
    }
}

class Vehicle {
    String brand;

    void start() {
        System.out.println("Vehicle is starting");
    }
}

class Car extends Vehicle {
    int numberOfDoors;

    void honk() {
        System.out.println("Car horn sounds");
    }
}

class Person {
    private String ssn;

    public String getSsn() {
        return ssn;
    }

    public void setSsn(String ssn) {
        this.ssn = ssn;
    }
}

class Employee extends Person {
    void printSsn() {
        // System.out.println(ssn); // Not accessible: ssn is private in Person
        System.out.println("SSN: " + getSsn()); // Access via public getter
    }
}

Why Use Inheritance in Design?

Inheritance is useful to:

However, it should be used thoughtfully; overusing inheritance can lead to rigid and tightly coupled designs.

Summary

The extends keyword in Java defines inheritance, allowing a subclass to acquire accessible methods and fields from a superclass. This mechanism enables efficient code reuse and models hierarchical relationships naturally. While private members and constructors are not inherited, subclasses can access superclass behavior and extend it with additional features.

Understanding what is inherited—and how to leverage inheritance properly—is critical for designing clean, maintainable object-oriented systems.

Index

4.2 Method Overriding

What Is Method Overriding?

Method overriding is a fundamental feature of object-oriented programming where a subclass provides its own implementation of a method that is already defined in its superclass. The goal is to change or extend the behavior inherited from the parent class.

This allows a subclass to customize or replace the behavior of methods it inherits, enabling flexible and dynamic behavior.

How Is Overriding Different from Overloading?

It’s important not to confuse method overriding with method overloading:

Example:

class Calculator {
    int add(int a, int b) { return a + b; }       // Overloading example
    int add(int a, int b, int c) { return a + b + c; }
}

class AdvancedCalculator extends Calculator {
    @Override
    int add(int a, int b) {                       // Overriding example
        System.out.println("Adding two numbers:");
        return super.add(a, b);
    }
}

The @Override Annotation

Java provides the @Override annotation, which you place just before a method to indicate it overrides a method from the superclass.

Benefits of @Override:

Example:

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Dog barks");
    }
}

If you accidentally write speek() instead of speak(), the compiler will flag it if you use @Override.

Example: Overriding Methods to Change Behavior

Consider a base class Vehicle and a subclass Car:

class Vehicle {
    void start() {
        System.out.println("Vehicle starting...");
    }
}

class Car extends Vehicle {
    @Override
    void start() {
        System.out.println("Car ignition started!");
    }
}

Usage:

Vehicle myVehicle = new Vehicle();
myVehicle.start();  // Output: Vehicle starting...

Car myCar = new Car();
myCar.start();      // Output: Car ignition started!

Here, the Car class overrides the start() method to provide behavior specific to cars, replacing the generic vehicle start message.

Rules for Overriding Methods

When overriding a method in Java, the following rules apply:

  1. Method Signature Must Match Exactly: The method name, return type, and parameter types must be identical to the superclass method.

  2. Access Level Cannot Be More Restrictive: The overriding method must have the same or broader access modifier. For example, if the superclass method is protected, the overriding method can be protected or public but not private.

  3. Return Type Must Be Compatible: The overriding method’s return type must be the same or a subtype (covariant return type) of the superclass method’s return type.

  4. Exception Handling: The overriding method can throw the same exceptions or fewer (more specific), but not new or broader checked exceptions.

  5. Static Methods Cannot Be Overridden: Static methods belong to the class, not instances, so they cannot be overridden. They can be re-declared in the subclass, which hides the superclass static method (called method hiding).

Polymorphism and Overriding

Method overriding is the key enabler of runtime polymorphism (also called dynamic dispatch) in Java. Polymorphism allows the JVM to decide at runtime which method implementation to invoke based on the actual object type, not the declared reference type.

Example:

Vehicle myVehicle = new Car();
myVehicle.start();  // Output: Car ignition started!

Even though myVehicle is declared as type Vehicle, the actual object is a Car. Because start() is overridden, the Car version is called at runtime, demonstrating polymorphism.

Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        Vehicle myVehicle = new Vehicle();
        myVehicle.start();  // Output: Vehicle starting...

        Car myCar = new Car();
        myCar.start();      // Output: Car ignition started!

        Vehicle polyVehicle = new Car();
        polyVehicle.start(); // Output: Car ignition started!
    }
}

class Vehicle {
    void start() {
        System.out.println("Vehicle starting...");
    }
}

class Car extends Vehicle {
    @Override
    void start() {
        System.out.println("Car ignition started!");
    }
}

Summary

Method overriding lets subclasses provide specialized behavior for methods inherited from their superclasses. By using the @Override annotation, developers get compile-time safety and improved code clarity.

The rules for overriding ensure consistent behavior and proper access control. Combined with polymorphism, overriding enables flexible and extensible designs where objects behave according to their actual types, not just their declared types.

Mastering method overriding is essential for building dynamic, maintainable, and scalable Java applications.

Index

4.3 super Keyword

In Java, the super keyword is a special reference that allows a subclass to access members (methods, constructors, or fields) of its immediate superclass. It acts as a bridge between the child and parent classes, enabling reuse and extension of functionality.

Using super() to Call Superclass Constructors

One of the most common uses of super is to invoke a superclass constructor from within a subclass constructor. This is important because constructors are not inherited; each class must explicitly initialize itself. Using super() ensures the superclass is properly initialized before the subclass adds its own initialization.

Example:

class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor called");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);  // Calls Animal’s constructor
        System.out.println("Dog constructor called");
    }
}

When you create a new Dog, the output will be:

Animal constructor called
Dog constructor called

This demonstrates that the superclass constructor runs first, setting up the name field, followed by the subclass constructor.

If you don’t explicitly call super(), Java inserts a no-argument super() call automatically—but this only works if the superclass has a no-argument constructor. Otherwise, a compile-time error occurs.

Invoking Superclass Methods Using super

Besides constructors, super allows subclasses to invoke overridden methods in the superclass. This is useful when you want to extend or reuse behavior rather than completely replace it.

Example:

class Vehicle {
    void start() {
        System.out.println("Vehicle is starting");
    }
}

class Car extends Vehicle {
    @Override
    void start() {
        super.start();  // Call superclass method
        System.out.println("Car ignition turned on");
    }
}

When calling start() on a Car object, the output is:

Vehicle is starting
Car ignition turned on

Here, the Car method calls the original Vehicle method first, then adds its own functionality, demonstrating behavior extension.

Accessing Superclass Fields with super

If the subclass has fields with the same name as the superclass, you can use super to disambiguate and access the superclass field:

class Parent {
    int value = 10;
}

class Child extends Parent {
    int value = 20;

    void printValues() {
        System.out.println("Child value: " + value);
        System.out.println("Parent value: " + super.value);
    }
}

Output:

Child value: 20
Parent value: 10
Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Buddy");
        System.out.println();

        Car car = new Car();
        car.start();
        System.out.println();

        Child child = new Child();
        child.printValues();
    }
}

class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor called");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);  // Calls Animal’s constructor
        System.out.println("Dog constructor called");
    }
}

class Vehicle {
    void start() {
        System.out.println("Vehicle is starting");
    }
}

class Car extends Vehicle {
    @Override
    void start() {
        super.start();  // Call superclass method
        System.out.println("Car ignition turned on");
    }
}

class Parent {
    int value = 10;
}

class Child extends Parent {
    int value = 20;

    void printValues() {
        System.out.println("Child value: " + value);
        System.out.println("Parent value: " + super.value);
    }
}

Common Pitfalls and Best Practices

Summary

The super keyword is a vital tool in inheritance, providing controlled access to superclass constructors, methods, and fields. It helps build clean, maintainable hierarchies by allowing subclasses to initialize and reuse superclass behavior effectively. Mastering super unlocks powerful ways to extend and customize class functionality in Java.

Index

4.4 Constructors and Inheritance

In Java, constructors are special methods responsible for initializing new objects. When inheritance is involved, understanding how constructors work in a class hierarchy is crucial for writing robust, maintainable code.

Constructor Invocation Order in Inheritance

When you create an instance of a subclass, constructors are called starting from the topmost superclass down to the subclass. This ensures that the superclass part of the object is fully initialized before the subclass adds its own initialization.

For example, if class A is the superclass and class B extends A, creating a new B object calls the constructor of A first, then B’s constructor.

This top-down invocation order maintains the integrity of the inheritance chain and prevents partially initialized objects.

Implicit and Explicit Calls to Superclass Constructors

Example:

class Animal {
    Animal() {
        System.out.println("Animal constructor");
    }
    
    Animal(String name) {
        System.out.println("Animal constructor with name: " + name);
    }
}

class Dog extends Animal {
    Dog() {
        super();  // Implicit if omitted
        System.out.println("Dog constructor");
    }
    
    Dog(String name) {
        super(name);  // Explicit call to superclass constructor with argument
        System.out.println("Dog constructor with name");
    }
}

Constructor Chaining Example

Dog dog1 = new Dog();
// Output:
// Animal constructor
// Dog constructor

Dog dog2 = new Dog("Buddy");
// Output:
// Animal constructor with name: Buddy
// Dog constructor with name

The Dog constructors invoke the Animal constructors explicitly with super(). This chaining ensures proper initialization of inherited state.

Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        Dog dog1 = new Dog();
        System.out.println();

        Dog dog2 = new Dog("Buddy");
    }
}

class Animal {
    Animal() {
        System.out.println("Animal constructor");
    }

    Animal(String name) {
        System.out.println("Animal constructor with name: " + name);
    }
}

class Dog extends Animal {
    Dog() {
        super();  // Implicit if omitted
        System.out.println("Dog constructor");
    }

    Dog(String name) {
        super(name);  // Explicit call to superclass constructor with argument
        System.out.println("Dog constructor with name");
    }
}

Why Constructors Are Not Inherited

Unlike regular methods, constructors are not inherited because:

  1. Constructors initialize the specific class they belong to. The superclass constructor prepares the superclass part of the object, while the subclass constructor handles subclass-specific initialization.

  2. Each class defines how its objects are constructed. Allowing constructor inheritance would blur the distinct initialization responsibilities between classes.

  3. Java enforces explicit constructor calls. This avoids confusion and ensures developers consciously decide how to initialize both superclass and subclass parts.

Summary

When creating objects in an inheritance hierarchy, constructors are invoked in a top-down manner—from superclass to subclass. Subclass constructors either implicitly or explicitly call superclass constructors using super(). This ensures the entire object is properly initialized.

Understanding that constructors are not inherited emphasizes the unique role constructors play: each class controls its own initialization. Constructor chaining through super() calls is a key tool for coordinating initialization across the inheritance tree.

Mastering constructor behavior in inheritance will help you write clear, predictable, and reliable Java code.

Index

4.5 Inheritance Best Practices

Inheritance is a powerful mechanism in Java, but it comes with responsibilities and potential pitfalls. Knowing when and how to use inheritance effectively is key to designing clean, maintainable software.

When to Use Inheritance vs. Composition

Inheritance expresses an “is-a” relationship—meaning the subclass should be a specialized version of the superclass. For example, a Car is a Vehicle, so inheritance makes sense.

However, if you want to express a “has-a” relationship, such as a Car has an Engine, composition (embedding objects as fields) is a better choice. Composition promotes greater flexibility by allowing objects to be assembled from interchangeable parts rather than fixed class hierarchies.

Rule of thumb:

Common Pitfalls of Inheritance

  1. Tight Coupling: Subclasses are tightly coupled to the superclass implementation. Changes in the superclass can inadvertently break subclass behavior, leading to fragile designs.

  2. Fragile Base Class Problem: If the superclass is modified, all subclasses might be affected. This problem grows as hierarchies become deep and complex.

  3. Inheritance for Code Reuse Alone: Avoid using inheritance solely to reuse code. This often leads to inappropriate relationships and violates the Liskov Substitution Principle (an “is-a” relationship must hold).

  4. Overusing Inheritance: Deep inheritance hierarchies become hard to understand and maintain. Favor composition and interfaces where appropriate.

Guidelines for Designing Inheritance Hierarchies

Examples of Good Inheritance Design Patterns

Index