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
publicandprivate -
Understanding the
thiskeyword
What You Will Learn
-
How to use the
extendskeyword to create a subclass -
The role of the
protectedaccess 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:
// 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.
// 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!");
}
}
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();
}
}
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 .
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
finalorstatic. (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 itpublic, but notprivate.
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.
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.");
}
}
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.
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
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
// 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
-
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
Engineobject inside theCarclass instead of makingCarextendEngine. - 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.
- Always Use @Override: Never omit this annotation when overriding methods. It is your first line of defense against typos and signature mismatches.
-
Make Parent Classes Abstract When Appropriate:
If a class like
AnimalorProductis only meant to be a base for subclasses and should never be instantiated directly, mark it asabstract. - 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
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
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());
}
}
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
extendskeyword. - 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
@Overrideannotation to prevent silent bugs. -
The
protectedaccess 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.