Master Java Inheritance and Class Hierarchies
Java Inheritance
Inheritance is one of the fundamental principles of object-oriented programming that allows a class to inherit properties and methods from another class. It promotes code reusability, establishes relationships between classes, and enables polymorphism.
What is Inheritance?
Inheritance creates an "is-a" relationship between classes, where a child class (subclass) inherits characteristics from a parent class (superclass). The child class can:
- Access non-private members of the parent class
- Override parent methods to provide specific implementations
- Add its own unique properties and methods
// Parent class (Superclass)
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");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// Child class (Subclass)
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Call parent constructor
this.breed = breed;
}
public void bark() {
System.out.println(name + " is barking");
}
}
// Usage
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
myDog.eat(); // Inherited from Animal
myDog.sleep(); // Inherited from Animal
myDog.bark(); // Specific to Dog
Single Inheritance
Java supports single inheritance, meaning a class can only extend one direct parent class:
public class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
public void start() {
System.out.println("Vehicle starting...");
}
}
public class Car extends Vehicle {
private int doors;
public Car(String brand, int year, int doors) {
super(brand, year);
this.doors = doors;
}
public void honk() {
System.out.println("Car honking!");
}
}
// This would cause a compilation error:
// public class SportsCar extends Vehicle, Car { } // NOT ALLOWED
The super Keyword
Calling Parent Constructor
public class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
public class Manager extends Employee {
private String department;
public Manager(String name, double salary, String department) {
super(name, salary); // Must be first statement
this.department = department;
}
}
Accessing Parent Methods
public class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
}
public class Circle extends Shape {
@Override
public void draw() {
super.draw(); // Call parent method
System.out.println("Drawing a circle");
}
}
// Output:
// Drawing a shape
// Drawing a circle
Accessing Parent Fields
public class Person {
protected String name = "Unknown";
}
public class Student extends Person {
private String name = "Student"; // Shadows parent field
public void printNames() {
System.out.println("Child name: " + this.name); // "Student"
System.out.println("Parent name: " + super.name); // "Unknown"
}
}
Method Overriding
Override parent methods to provide specific implementations:
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
public void move() {
System.out.println("The animal moves");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
@Override
public void move() {
System.out.println("The cat prowls silently");
}
// Cat-specific method
public void purr() {
System.out.println("The cat purrs contentedly");
}
}
public class Bird extends Animal {
@Override
public void makeSound() {
System.out.println("Tweet tweet!");
}
@Override
public void move() {
System.out.println("The bird flies");
}
}
Rules for Method Overriding
public class Parent {
// Protected method can be overridden with protected or public
protected void method1() { }
// Public method can only be overridden with public
public void method2() { }
// Final methods cannot be overridden
public final void method3() { }
// Static methods are hidden, not overridden
public static void staticMethod() {
System.out.println("Parent static method");
}
}
public class Child extends Parent {
@Override
public void method1() { } // Valid: protected -> public
@Override
public void method2() { } // Valid: public -> public
// @Override
// public void method3() { } // ERROR: Cannot override final method
// This hides the parent static method
public static void staticMethod() {
System.out.println("Child static method");
}
}
Abstract Classes
Abstract classes provide a partial implementation that must be extended:
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method - must be implemented by subclasses
public abstract double calculateArea();
// Concrete method - inherited by subclasses
public void displayInfo() {
System.out.println("Shape color: " + color);
System.out.println("Area: " + calculateArea());
}
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
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;
}
}
// Usage
Shape rectangle = new Rectangle("Red", 5.0, 3.0);
Shape circle = new Circle("Blue", 4.0);
rectangle.displayInfo(); // Uses Rectangle's implementation
circle.displayInfo(); // Uses Circle's implementation
Multilevel Inheritance
Classes can form inheritance chains:
public class LivingBeing {
protected boolean isAlive = true;
public void breathe() {
System.out.println("Breathing...");
}
}
public class Animal extends LivingBeing {
protected String species;
public Animal(String species) {
this.species = species;
}
public void move() {
System.out.println("Moving...");
}
}
public class Mammal extends Animal {
private boolean hasHair = true;
public Mammal(String species) {
super(species);
}
public void produceMilk() {
System.out.println("Producing milk...");
}
}
public class Dog extends Mammal {
private String breed;
public Dog(String breed) {
super("Canine");
this.breed = breed;
}
public void bark() {
System.out.println("Woof!");
}
}
// Dog inherits from all levels
Dog myDog = new Dog("Labrador");
myDog.breathe(); // From LivingBeing
myDog.move(); // From Animal
myDog.produceMilk(); // From Mammal
myDog.bark(); // From Dog
Real-World Examples
1. Employee Hierarchy
public abstract class Employee {
protected String name;
protected String employeeId;
protected double baseSalary;
public Employee(String name, String employeeId, double baseSalary) {
this.name = name;
this.employeeId = employeeId;
this.baseSalary = baseSalary;
}
public abstract double calculateSalary();
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("ID: " + employeeId);
System.out.println("Salary: $" + calculateSalary());
}
}
public class FullTimeEmployee extends Employee {
private double benefits;
public FullTimeEmployee(String name, String id, double baseSalary, double benefits) {
super(name, id, baseSalary);
this.benefits = benefits;
}
@Override
public double calculateSalary() {
return baseSalary + benefits;
}
}
public class PartTimeEmployee extends Employee {
private int hoursWorked;
private double hourlyRate;
public PartTimeEmployee(String name, String id, int hours, double rate) {
super(name, id, 0); // No base salary
this.hoursWorked = hours;
this.hourlyRate = rate;
}
@Override
public double calculateSalary() {
return hoursWorked * hourlyRate;
}
}
public class Contractor extends Employee {
private double contractAmount;
public Contractor(String name, String id, double contractAmount) {
super(name, id, 0);
this.contractAmount = contractAmount;
}
@Override
public double calculateSalary() {
return contractAmount;
}
}
2. Vehicle System
public abstract class Vehicle {
protected String make;
protected String model;
protected int year;
protected double fuelLevel;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.fuelLevel = 100.0; // Start with full tank
}
public abstract void start();
public abstract double getFuelEfficiency();
public void refuel() {
this.fuelLevel = 100.0;
System.out.println("Vehicle refueled");
}
public double calculateRange() {
return fuelLevel * getFuelEfficiency();
}
}
public class Car extends Vehicle {
private int numberOfDoors;
public Car(String make, String model, int year, int doors) {
super(make, model, year);
this.numberOfDoors = doors;
}
@Override
public void start() {
System.out.println("Car engine starting with key ignition");
}
@Override
public double getFuelEfficiency() {
return 25.0; // 25 miles per gallon
}
}
public class Motorcycle extends Vehicle {
private int engineSize;
public Motorcycle(String make, String model, int year, int engineSize) {
super(make, model, year);
this.engineSize = engineSize;
}
@Override
public void start() {
System.out.println("Motorcycle starting with kick start");
}
@Override
public double getFuelEfficiency() {
return 45.0; // 45 miles per gallon
}
}
public class ElectricCar extends Car {
private double batteryCapacity;
public ElectricCar(String make, String model, int year, int doors, double battery) {
super(make, model, year, doors);
this.batteryCapacity = battery;
}
@Override
public void start() {
System.out.println("Electric car starting silently");
}
@Override
public double getFuelEfficiency() {
return 100.0; // 100 miles per charge
}
@Override
public void refuel() {
System.out.println("Charging electric vehicle");
super.refuel(); // Reuse parent logic
}
}
Best Practices
1. Favor Composition Over Inheritance
Sometimes composition is better than inheritance:
// Instead of inheritance
public class Bird extends Animal {
public void fly() { /* flying logic */ }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// Consider composition
public class Animal {
private MovementBehavior movementBehavior;
public Animal(MovementBehavior behavior) {
this.movementBehavior = behavior;
}
public void move() {
movementBehavior.move();
}
}
interface MovementBehavior {
void move();
}
class FlyingBehavior implements MovementBehavior {
public void move() { System.out.println("Flying"); }
}
class SwimmingBehavior implements MovementBehavior {
public void move() { System.out.println("Swimming"); }
}
2. Use @Override Annotation
Always use @Override
when overriding methods:
public class Parent {
public void method() { }
}
public class Child extends Parent {
@Override // Helps catch errors at compile time
public void method() { }
// @Override // This will cause compilation error
// public void methd() { } // Typo in method name
}
3. Design for Inheritance or Prohibit It
// Design for inheritance - document and provide hooks
public class ExtendableClass {
/**
* Hook for subclasses to customize behavior.
* Called by process() method.
*/
protected void beforeProcess() {
// Default implementation - can be overridden
}
public final void process() {
beforeProcess();
// Core processing logic
afterProcess();
}
protected void afterProcess() {
// Default implementation - can be overridden
}
}
// Prohibit inheritance with final
public final class UtilityClass {
private UtilityClass() { } // Prevent instantiation
public static void utilityMethod() {
// Utility logic
}
}
Common Pitfalls
1. Calling Overridable Methods in Constructor
public class Parent {
public Parent() {
init(); // Dangerous - calls overridable method
}
protected void init() {
System.out.println("Parent init");
}
}
public class Child extends Parent {
private String value = "initialized";
@Override
protected void init() {
System.out.println("Child init: " + value); // value might be null!
}
}
2. Breaking Liskov Substitution Principle
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
// This violates LSP - Square is not substitutable for Rectangle
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = this.height = width; // Changes both dimensions
}
@Override
public void setHeight(int height) {
this.width = this.height = height; // Changes both dimensions
}
}
Summary
Java inheritance enables:
- Code Reusability: Share common functionality across classes
- Polymorphism: Treat objects of different types uniformly
- Abstraction: Define common interfaces through abstract classes
- Extensibility: Build upon existing functionality
Key concepts:
- Single inheritance with
extends
keyword - Method overriding with
@Override
annotation super
keyword for accessing parent class members- Abstract classes for partial implementations
- Design considerations for maintainable inheritance hierarchies
Use inheritance judiciously - favor composition when the relationship isn't truly "is-a" and always design for extensibility or explicitly prevent it.