Java Inheritance: Complete Guide with Examples and Best Practices

Learn how to create parent-child relationships between classes, reuse code effectively, and implement method overriding to build flexible Java applications.

Introduction to Inheritance

In the real world, objects often share characteristics while retaining unique features. A car and a motorcycle are both vehicles—they have wheels, engines, and can move. However, a car has four wheels and a steering wheel, while a motorcycle has two wheels and handlebars.

Inheritance in Java is the mechanism that models this exact relationship. It allows a new class (the child or subclass ) to inherit the fields and methods of an existing class (the parent or superclass ). The child class automatically gets everything the parent class has, and can then add its own specific fields and methods, or change (override) the behavior of the inherited ones.

This establishes an "IS-A" relationship. A Car is a Vehicle. A Dog is an Animal. If an "IS-A" sentence doesn't make logical sense for two classes, inheritance is likely the wrong tool to use.

Why Use Inheritance?

  • Code Reusability: Write common logic once in the parent class, and all child classes inherit it automatically without duplicating code.
  • Polymorphism Foundation: Inheritance allows you to treat different child classes as their parent type, enabling flexible and extensible program design.
  • Logical Structure: It creates a natural, hierarchical organization for your codebase that mirrors real-world relationships.

Prerequisites

Before tackling inheritance, ensure you are comfortable with:

  • Creating and instantiating classes and objects
  • Writing methods , including method overloading
  • Using access modifiers, specifically public and private
  • Understanding the this keyword

What You Will Learn

  • How to use the extends keyword to create a subclass
  • The role of the protected access modifier in inheritance
  • How constructor chaining works using super()
  • How to override parent methods using @Override
  • How to call a parent's method from within a child class
  • When to use inheritance versus other techniques like composition

The Basics of Inheritance

When a class inherits from another, it acquires all the public and protected fields and methods of the parent. private members are not directly accessible to the child class—they exist in memory, but the child must use the parent's public getters or setters to interact with them.

Java supports single class inheritance . A class can only extend one parent class. This prevents the "Diamond Problem" (ambiguity that arises when a class inherits from two classes that have the same method). However, a parent class can have unlimited children, and a child class can itself be a parent to another class (creating a multi-level hierarchy).

At the very top of every Java inheritance hierarchy sits the Object class. If you don't explicitly use the extends keyword, Java implicitly extends java.lang.Object . This is why every Java object has methods like toString() , equals() , and hashCode() .

Inheritance Syntax

You establish inheritance using the extends keyword in the child class declaration:

Java Syntax
// Parent Class (Superclass)
public class ParentClass {
    protected DataType fieldName;
    public void parentMethod() { /* ... */ }
}

// Child Class (Subclass)
public class ChildClass extends ParentClass {
    // Inherits fieldName and parentMethod automatically
    
    public void childMethod() { /* ... */ }
}

The Protected Access Modifier

You will often see the protected keyword in parent classes. A protected field or method is accessible within its own package, and also accessible from subclasses, even if those subclasses are in different packages. It is the perfect middle ground between private (too restrictive for children) and public (too open for the outside world).

Simple Example: Animal Hierarchy

Let's model a simple Animal hierarchy. The parent class defines common attributes, while the child class adds specific ones.

Java
// Parent Class
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

// Child Class
public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // Must call parent constructor first!
        this.breed = breed;
    }

    public void bark() {
        System.out.println(name + " says Woof!");
    }
}
Java — Main Class
public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog("Buddy", "Golden Retriever");
        
        // Calling a method defined in the Dog class
        myDog.bark();
        
        // Calling a method INHERITED from the Animal class
        myDog.eat();
    }
}
Output
Buddy says Woof! Buddy is eating.

How It Works: The super() Call

In the Dog constructor, the very first line is super(name); . This is called constructor chaining .

Because a child class inherits from the parent, the parent's state must be initialized before the child adds to it. Java enforces this strictly: the call to super() must be the very first statement in a child class constructor.

If you do not write super() explicitly, Java will secretly insert a call to super() (the parent's no-argument constructor) for you. However, if the parent class only has a parameterized constructor (like our Animal class does), and you forget to write super(name) , the compiler will throw an error because it cannot find the default no-argument constructor.

Method Overriding

Inheritance allows a child class to provide its own specific implementation of a method that is already provided by its parent class. This is called method overriding .

Java
public class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

When you call myCat.makeSound() , Java executes the Cat version, not the Animal version.

Rules for Overriding

  • The method in the child must have the exact same name, parameters, and return type (or a covariant return type—a subclass of the original return type).
  • You cannot override a method marked as final or static . (If you change a static method in a child class, it is called method hiding , not overriding).
  • The access level in the child cannot be more restrictive than the parent. If the parent method is protected , the child can make it public , but not private .

Always Use @Override

Always prefix overridden methods with the @Override annotation. It does not change how the code runs, but it tells the compiler to check that you are actually overriding a parent method. If you misspell the method name (e.g., public void makesound() ), the compiler will catch it immediately. Without @Override , Java would assume you are creating a brand new, separate method, leading to silent bugs.

Calling Parent Methods with super

Sometimes, when you override a method, you don't want to completely replace the parent's logic—you just want to add to it. You can call the parent's version of the method using the super keyword followed by a dot.

Java
public class Vehicle {
    public void start() {
        System.out.println("Engine is turning over...");
        System.out.println("Vehicle is ready.");
    }
}

public class ElectricCar extends Vehicle {
    @Override
    public void start() {
        // Call the parent's start logic first
        super.start();
        
        // Then add child-specific logic
        System.out.println("Battery levels checked. Systems online.");
    }
}
Output (when calling new ElectricCar().start())
Engine is turning over... Vehicle is ready. Battery levels checked. Systems online.

Real-World Example: E-Commerce Products

In an e-commerce application, you might sell physical items and digital downloads. Both are "Products" with a name and price, but they calculate shipping very differently.

Java
public abstract class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public double getPrice() { return price; }
    public String getName() { return name; }

    // Force subclasses to define their own shipping logic
    public abstract double calculateShipping();

    public void displayOrderSummary() {
        System.out.printf("Product: %s | Price: $%.2f | Shipping: $%.2f%n", 
            name, price, calculateShipping());
    }
}

public class PhysicalProduct extends Product {
    private double weightKg;

    public PhysicalProduct(String name, double price, double weightKg) {
        super(name, price);
        this.weightKg = weightKg;
    }

    @Override
    public double calculateShipping() {
        return weightKg * 5.00; // $5 per kg
    }
}

public class DigitalProduct extends Product {

    public DigitalProduct(String name, double price) {
        super(name, price);
    }

    @Override
    public double calculateShipping() {
        return 0.0; // No shipping for digital goods
    }
}

This design is powerful because the displayOrderSummary() method is written only once in the parent class, yet it correctly calls the appropriate calculateShipping() logic depending on whether the actual object is a PhysicalProduct or a DigitalProduct .

Common Mistakes to Avoid

Mistake 1: Forgetting super() When the Parent Has No Default Constructor

Java — Incorrect
public class Parent {
    public Parent(String s) { }
}

public class Child extends Parent {
    public Child() {
        // COMPILE ERROR: Implicit super() is undefined.
        // Parent has no no-argument constructor.
    }
}

Mistake 2: Using this() and super() in the Same Constructor

Both this() (calling another constructor in the same class) and super() (calling the parent constructor) must be the very first line of a constructor. Therefore, you cannot use both in the same constructor. You can, however, chain to another constructor using this() , and have that constructor call super() .

Mistake 3: Trying to Inherit Multiple Classes

Java — Incorrect
// COMPILE ERROR: Java does not support multiple class inheritance
public class Hybrid extends Car, Boat { }

If you need to combine behaviors from multiple sources, use Interfaces instead.

Best Practices

  1. Favor Composition Over Inheritance: The phrase "Favor composition over inheritance" is a core OOP principle. If a relationship is better described as "HAS-A" rather than "IS-A", use composition. For example, a Car has an Engine, it is not an Engine. Put an Engine object inside the Car class instead of making Car extend Engine .
  2. Keep Hierarchies Shallow: Deep inheritance trees (e.g., A -> B -> C -> D -> E) become fragile and hard to understand. Try to limit inheritance to 2 or 3 levels maximum.
  3. Always Use @Override: Never omit this annotation when overriding methods. It is your first line of defense against typos and signature mismatches.
  4. Make Parent Classes Abstract When Appropriate: If a class like Animal or Product is only meant to be a base for subclasses and should never be instantiated directly, mark it as abstract .
  5. Use Protected Sparingly: Exposing state even to subclasses can break encapsulation. Prefer passing data up via constructors and using protected methods (like hooks) rather than protected fields .

Performance Considerations

From a pure execution speed perspective, inheritance has minimal overhead. However, there are design-related performance implications to keep in mind:

  • Memory Footprint: An object of a subclass contains all the fields of its parent classes, plus its own. If your parent classes are bloated with fields that only some subclasses need, you waste memory. Keep base classes lean.
  • Virtual Method Invocation (VMI): When you call an overridden method on a parent reference, Java uses dynamic dispatch to figure out which child method to run at runtime. This is slightly slower than a standard static method call, but modern JVMs use advanced techniques (like inlining) to optimize this to the point where it is rarely a bottleneck.

Exercises

Exercise 1: The Vehicle Hierarchy

Create a Vehicle class with brand and year fields, and a method startEngine() that prints "Vroom!". Create two subclasses: Car (adds numDoors ) and Motorcycle (adds hasSidecar ). Override startEngine() in Motorcycle to print "Brap brap!". Instantiate both and call their methods.

Exercise 2: Employee Payroll System

Create an abstract Employee class with name and an abstract method double calculatePay() . Create a FullTimeEmployee (adds annualSalary , pay is salary / 12) and a Contractor (adds hourlyRate and hoursWorked, pay is rate * hours). Print the monthly pay for both.

Solutions

Solution to Exercise 1: The Vehicle Hierarchy

Java
public class Vehicle {
    protected String brand;
    protected int year;

    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }

    public void startEngine() {
        System.out.println("Vroom!");
    }
}

public class Car extends Vehicle {
    private int numDoors;

    public Car(String brand, int year, int numDoors) {
        super(brand, year);
        this.numDoors = numDoors;
    }
}

public class Motorcycle extends Vehicle {
    private boolean hasSidecar;

    public Motorcycle(String brand, int year, boolean hasSidecar) {
        super(brand, year);
        this.hasSidecar = hasSidecar;
    }

    @Override
    public void startEngine() {
        System.out.println("Brap brap!");
    }
}

// Main class to test
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car("Toyota", 2022, 4);
        Motorcycle myBike = new Motorcycle("Harley", 2021, false);

        myCar.startEngine();   // Prints: Vroom!
        myBike.startEngine(); // Prints: Brap brap!
    }
}

Explanation: Car inherits startEngine() unchanged. Motorcycle overrides it to provide its own implementation. Both successfully access brand and year because they are marked as protected in the parent class.

Solution to Exercise 2: Employee Payroll System

Java
public abstract class Employee {
    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() { return name; }

    public abstract double calculatePay();
}

public class FullTimeEmployee extends Employee {
    private double annualSalary;

    public FullTimeEmployee(String name, double annualSalary) {
        super(name);
        this.annualSalary = annualSalary;
    }

    @Override
    public double calculatePay() {
        return annualSalary / 12;
    }
}

public class Contractor extends Employee {
    private double hourlyRate;
    private double hoursWorked;

    public Contractor(String name, double hourlyRate, double hoursWorked) {
        super(name);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }

    @Override
    public double calculatePay() {
        return hourlyRate * hoursWorked;
    }
}

// Main class to test
public class Main {
    public static void main(String[] args) {
        Employee emp1 = new FullTimeEmployee("Alice", 120000);
        Employee emp2 = new Contractor("Bob", 50.00, 80);

        // Polymorphism in action: same method call, different behavior
        System.out.printf("%s's pay: $%.2f%n", emp1.getName(), emp1.calculatePay());
        System.out.printf("%s's pay: $%.2f%n", emp2.getName(), emp2.calculatePay());
    }
}
Output
Alice's pay: $10000.00 Bob's pay: $4000.00

Explanation: Notice how in the main method, both emp1 and emp2 are declared as type Employee . Despite this, calling calculatePay() executes the specific logic belonging to the actual child object ( FullTimeEmployee or Contractor ). This is polymorphism in action, powered by inheritance.

Summary

  • Inheritance models an "IS-A" relationship using the extends keyword.
  • Java only supports single class inheritance (one parent per child).
  • The super() keyword calls the parent constructor and must be the first line of the child constructor.
  • Method overriding allows a child to provide a specific implementation of a parent method.
  • Always use the @Override annotation to prevent silent bugs.
  • The protected access modifier allows members to be seen by child classes.
  • Prefer composition over inheritance for "HAS-A" relationships to keep your code flexible.

Frequently Asked Questions

Inheritance (using extends ) is for an "IS-A" relationship where a child acquires the actual implementation (code) of a parent. Interfaces (using implements ) define a "CAN-DO" contract—a list of method signatures without code. Java restricts you to extending only one class, but you can implement multiple interfaces, making interfaces the go-to solution for sharing capabilities across unrelated classes.

No, Java does not support multiple class inheritance. You cannot write class A extends B, C . This restriction was intentionally designed to avoid the "Diamond Problem," a complexity that occurs when two parent classes have conflicting implementations of the same method. To achieve multiple inheritance of type, you implement multiple interfaces instead.

The super keyword has two primary uses. First, super(args) calls a constructor in the immediate parent class to initialize inherited state. Second, super.methodName() calls a method from the parent class, which is commonly used inside an overridden method when you want to execute the parent's logic before or after the child's custom logic.

Method overriding is when a subclass provides its own specific implementation of a method that is already defined in its superclass. The method in the subclass must have the exact same name, return type, and parameter list as the method in the parent. It allows a child class to alter or extend the behavior defined by the parent.

The @Override annotation instructs the compiler to verify that you are actually overriding a method from a parent class. If you accidentally misspell the method name or provide the wrong parameters, the compiler will throw an error immediately. Without it, the compiler treats your method as a completely new method, which can cause confusing bugs at runtime when the parent's method gets called instead of yours.