Search⌘ K
AI Features

Understanding Inheritance

Learn how inheritance in Java lets you create class hierarchies to reuse code and reduce redundancy. Understand the role of the extends keyword, protected access modifier, and super keyword for constructors. This lesson helps you grasp how subclasses inherit from superclasses, override methods, and build flexible, logical application designs.

When building complex applications, we often find ourselves writing the same code for different classes. If we are building a payroll system, we might define a Manager class and a Developer class. Both need names, IDs, and salaries. Copying and pasting this logic leads to errors and makes updates difficult. If we need to change how salaries are stored, we have to update it in multiple places.

Java solves this with inheritance, a mechanism that allows a new class to adopt the properties and behaviors of an existing class. This creates a logical hierarchy, reduces redundancy, and enables us to write cleaner, more maintainable code.

What is inheritance?

Inheritance is the mechanism by which a subclass inherits members from a superclass. The subclass contains the superclass part of the object, but access to inherited members is still controlled by access modifiers. This allows us to define a general class with common features and create specific classes that extend it.

We use specific terminology to describe this relationship:

  • Superclass (parent): The class being extended. It holds the common attributes and behaviors.

  • Subclass (child): The class that extends the superclass. It inherits features from the parent and can add its own specific features.

Inheritance models an “is-a” relationship. If we have a class Vehicle and a subclass Car, we can say “A Car is a Vehicle.” If this sentence makes logical sense, inheritance is likely the right design choice.

The protected modifier

Before we write code to extend classes, we must introduce a new access modifier: protected.

Previously, we used private (accessible only within the class) and public (accessible everywhere). However, inheritance creates a special “family” relationship. We often want a child class to access the parent’s internal state, but we do not want to expose that state to the entire world.

The protected modifier solves this. A member marked as protected is accessible:

  1. Within the same package (like package-private).

  2. By subclasses, even if they are in a different package.

Here is how protected compares to the other access modifiers:

Modifier

Access within class

Access within subclass

Access within package

Global access

private

Yes

No

No

No

protected

Yes

Yes

Yes

No

(no keyword)

Yes

Only for subclasses in the same package

Yes

No

public

Yes

Yes

Yes

Yes

Think of protected as family secrets: shared between parents and children, but hidden from strangers.

Extending classes with extends

We establish inheritance in Java using the extends keyword. When a subclass extends a superclass, it inherits the parent’s members, but what it can access depends on visibility: public and protected are accessible, package-private is accessible only in the same package, and Private is not directly accessible.

Let’s look at a basic hierarchy where a Car inherits state from a Vehicle.

Java 25
class Vehicle {
// protected allows subclasses to access this directly
protected String brand = "Generic Brand";
public void honk() {
System.out.println("Tuut, tuut!");
}
}
class Car extends Vehicle {
public int numberOfDoors = 4;
public void displayBrand() {
// We can access 'brand' directly because it is protected
System.out.println("Vehicle Brand: " + brand);
}
}
public class Main {
public static void main(String[] args) {
var myCar = new Car();
// Accessing inherited method
myCar.honk();
// Accessing inherited field via subclass method
myCar.displayBrand();
// Accessing subclass-specific field
System.out.println("Doors: " + myCar.numberOfDoors);
}
}

Note: In real-world code, fields are usually kept private and exposed through protected or public methods. We use a protected field here only to demonstrate how inheritance access works.

  • Line 3: We use protected for brand. If we used private, Car would not be able to access it directly.

  • Line 10: The Car class uses extends Vehicle to inherit functionality.

  • Line 15: Inside Car, we access brand as if it were defined in Car itself. This works because of the protected modifier.

  • Lines 24–27: We create an instance of Car and call honk(), which is inherited from Vehicle.

The super keyword and constructors

Constructors are not inherited. When we create an instance of a subclass, Java must essentially “build” the parent part of the object before it builds the child part. This ensures that any logic the parent relies on, such as initializing fields, executes first.

We use the super keyword to call a constructor in the superclass.

  • Implicit call: If we do not call super() explicitly, Java automatically inserts a call to the no-argument constructor of the superclass. This works only if the superclass has an accessible no-argument constructor. If it does not, the subclass must explicitly call an existing superclass constructor.

  • Explicit call: If the superclass does not have a no-argument constructor (e.g., it only has a constructor that takes arguments), the subclass must explicitly call super(arguments) as the very first line of its own constructor.

Here is how we handle parameterized constructors in a hierarchy.

Java 25
class Employee {
protected String name;
// Parent constructor requires a name
public Employee(String name) {
this.name = name;
System.out.println("Employee constructor called");
}
}
class Manager extends Employee {
private int teamSize;
public Manager(String name, int teamSize) {
// Must be the first line
super(name);
this.teamSize = teamSize;
System.out.println("Manager constructor called");
}
public void displayInfo() {
System.out.println(name + " manages " + teamSize + " people.");
}
}
public class Main {
public static void main(String[] args) {
var mgr = new Manager("Alice", 5);
mgr.displayInfo();
}
}
  • Line 5: The Employee constructor takes a String argument. It does not have a default no-argument constructor.

  • Line 14: The Manager constructor accepts both name (for the parent) and teamSize (for itself).

  • Line 16: We call super(name) to pass the name up to the Employee constructor. If we omitted this line, the code would not compile.

  • Line 17: After the parent is initialized, we initialize Manager-specific fields.

  • Line 28: Creating a Manager triggers the chain: Employee initializes first, followed by Manager.

Accessing superclass members

If a subclass declares a field with the same name, it hides the superclass field. If it declares an instance method with the same signature, it overrides the superclass method. If we need to reference the member belonging to the parent class explicitly, we use the super keyword followed by the dot operator (e.g., super.fieldName or super.methodName()).

This is distinct from this, which refers to the current instance. super specifically looks up the inheritance chain.

Java 25
class Message {
public void print() {
System.out.println("Message from Parent");
}
}
class Alert extends Message {
public void print() {
// Calls the print method from the parent class
super.print();
System.out.println("Additional Alert Info");
}
}
public class Main {
public static void main(String[] args) {
var alert = new Alert();
alert.print();
}
}
  • Line 2: The parent class Message has a print method.

  • Line 8: The Alert class also defines a print method.

  • Line 10: Inside the child’s method, super.print() explicitly executes the code inside Message.

  • Line 11: The child then executes its own additional logic.

  • Line 17: Calling print() on the object triggers the child’s method, which internally leverages the parent’s behavior.

The single inheritance rule

Java follows a single inheritance model for classes. A class can extend only one direct superclass. We cannot write class C extends A, B. This rule exists to avoid the “Diamond Problem”. If classes A and B both had a method execute(), and C inherited from both, Java would not know which execute() method to run.

However, Java supports multi-level inheritance. A class can inherit from a class that is already a subclass of another. The inheritance chain is transitive: if C extends B and B extends A, then C is also an A and inherits everything from both B and A.

Here is an example of a multi-level hierarchy: Animal \to Mammal \to Dog.

Java 25
class Animal {
void eat() {
System.out.println("Eating...");
}
}
class Mammal extends Animal {
void breathe() {
System.out.println("Breathing...");
}
}
class Dog extends Mammal {
void bark() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
var dog = new Dog();
dog.bark(); // Defined in Dog
dog.breathe(); // Inherited from Mammal
dog.eat(); // Inherited from Animal (Grandparent)
}
}
  • Line 1: Animal is the root superclass.

  • Line 7: Mammal extends Animal, inheriting eat().

  • Line 13: Dog extends Mammal, inheriting both breathe() (direct parent) and eat() (grandparent).

  • Lines 23–25: The dog object can call methods from all three levels of the hierarchy.

Inheritance allows us to structure code logically, reuse existing functionality, and enforce relationships between classes. We now know how to extend classes using extends, share internal state securely using protected, manage initialization with super(), and build deep class hierarchies.