Inheritance: Code Reuse
Duration: 45 min
Inheritance: Code Reuse
Duration: 45 min
Introduction
Inheritance allows you to create new classes based on existing ones, inheriting their fields and methods. This eliminates code duplication and establishes relationships between classes. A class can extend another to specialize its behavior or add new functionality. Inheritance enables the IS-A relationship: a Dog IS-A Animal, a SavingsAccount IS-A BankAccount.
This module covers extending classes with extends, method overriding, the super keyword, abstract classes, protected access, and designing class hierarchies. You'll learn when inheritance is appropriate and how to use it effectively.
Extends Keyword and Basic Inheritance
The extends keyword creates a parent-child relationship. The child (subclass) inherits all public and protected members from the parent (superclass).
// Parent class (superclass)
public class Vehicle {
protected String brand; // Protected - accessible to subclasses
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
public void start() {
System.out.println(brand + " engine starting...");
}
public void stop() {
System.out.println(brand + " engine stopping...");
}
public void describe() {
System.out.println("Vehicle: " + brand + " (" + year + ")");
}
}// Child class (subclass)
public class Car extends Vehicle {
private int numDoors;
// Constructor must call super()
public Car(String brand, int year, int numDoors) {
super(brand, year); // Call parent constructor
this.numDoors = numDoors;
}
// New method in subclass
public void openTrunk() {
System.out.println("Trunk opened");
}
// Override parent method
@Override
public void describe() {
super.describe(); // Call parent version
System.out.println("Doors: " + numDoors);
}
}
public class InheritanceDemo {
public static void main(String[] args) {
Car myCar = new Car("Toyota", 2023, 4);
myCar.start(); // Inherited method
myCar.openTrunk(); // New method
myCar.describe(); // Overridden method
myCar.stop(); // Inherited method
}
}
The child class automatically gets all parent methods and fields. You can override methods to change behavior. Use super() to call the parent constructor and super.methodName() to call parent methods.
Method Overriding
Overriding replaces a parent method with a specialized version in the child. The signature must match exactly; only the body changes.
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}public class Dog extends Animal {
public Dog(String name) {
super(name);
}
// Override to provide dog-specific implementation
@Override
public void makeSound() {
System.out.println(name + " barks: Woof! Woof!");
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
// Override with cat-specific implementation
@Override
public void makeSound() {
System.out.println(name + " meows: Meow!");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
Animal dog = new Dog("Rex");
Animal cat = new Cat("Whiskers");
dog.makeSound(); // "Rex barks"
cat.makeSound(); // "Whiskers meows"
dog.sleep(); // "Rex is sleeping" (inherited, not overridden)
cat.sleep(); // "Whiskers is sleeping" (inherited)
}
}
The @Override annotation is optional but recommended—it helps catch mistakes (e.g., typos in method name). Overridden methods maintain the same signature but different implementation.
Super Keyword
super refers to the parent class. Use it to call parent constructors and methods.
public class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public double getAnnualBonus() {
return salary * 0.1; // 10% bonus
}
public void displayInfo() {
System.out.println("Employee: " + name + ", Salary: $" + salary);
}
}public class Manager extends Employee {
private int teamSize;
public Manager(String name, double salary, int teamSize) {
super(name, salary); // Call parent constructor
this.teamSize = teamSize;
}
@Override
public double getAnnualBonus() {
// Call parent implementation and add extra bonus
double baseBonus = super.getAnnualBonus();
double teamBonus = 1000 * teamSize;
return baseBonus + teamBonus;
}
@Override
public void displayInfo() {
super.displayInfo(); // Call parent implementation first
System.out.println("Team size: " + teamSize);
System.out.println("Annual bonus: $" + getAnnualBonus());
}
}
public class SuperDemo {
public static void main(String[] args) {
Employee emp = new Employee("Alice", 50000);
emp.displayInfo();
System.out.println("Bonus: $" + emp.getAnnualBonus());
System.out.println();
Manager mgr = new Manager("Bob", 80000, 5);
mgr.displayInfo();
}
}
Using super allows you to build on parent behavior instead of completely replacing it. This is powerful for specialization—extend functionality rather than reimplementing from scratch.
Abstract Classes
Abstract classes cannot be instantiated; they provide a template for subclasses. Use abstract for classes and methods.
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method - subclass must implement
public abstract double getArea();
public abstract double getPerimeter();
// Concrete method - shared by all subclasses
public void describe() {
System.out.println("Shape color: " + color);
System.out.printf("Area: %.2f, Perimeter: %.2f%n", getArea(), getPerimeter());
}
}public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI radius radius;
}
@Override
public double getPerimeter() {
return 2 Math.PI radius;
}
}
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 getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
public class AbstractDemo {
public static void main(String[] args) {
// Shape s = new Shape("red"); // ERROR: Cannot instantiate abstract class
Shape circle = new Circle("red", 5);
circle.describe();
Shape rect = new Rectangle("blue", 4, 6);
rect.describe();
}
}
Abstract classes enforce that subclasses provide implementations for critical methods. They're perfect for defining class hierarchies where subclasses have different implementations but shared behavior.
Protected Access
protected members are accessible to subclasses and other classes in the same package. It's more restrictive than public but less restrictive than private.
public class BankAccount {
private String accountNumber; // Only in this class
protected double balance; // Subclasses can access
public String accountHolder; // Everywhere
protected BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
protected void updateBalance(double amount) {
balance += amount;
}
}public class SavingsAccount extends BankAccount {
private double interestRate;
public SavingsAccount(String accountNumber, double balance, double rate) {
super(accountNumber, balance);
this.interestRate = rate;
}
public void addInterest() {
// Can access protected balance
updateBalance(balance * interestRate);
System.out.println("Interest added. New balance: $" + balance);
}
public void display() {
// Cannot access private accountNumber
// System.out.println(accountNumber); // ERROR
// Can access protected balance
System.out.println("Savings Account Balance: $" + balance);
System.out.println("Interest Rate: " + (interestRate * 100) + "%");
}
}
Protected methods are part of the subclass interface—they're intended for subclass use. Private methods are implementation details—subclasses shouldn't depend on them.
Design Principles
IS-A Relationship
Inheritance represents an IS-A relationship. Use it only when genuinely true.
// Good: Dog IS-A Animal (true relationship)
class Dog extends Animal { }// Bad: Car IS-A Engine (not true - Car HAS-A Engine)
class Car extends Engine { } // Wrong!
// Correct approach for HAS-A:
class Car {
private Engine engine;
}
Liskov Substitution Principle
Subclasses should be usable wherever parent classes are expected.
// This violates LSP
class Bird {
public void fly() { }
}class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
// Better approach:
class Bird {
public void move() { }
}
class Penguin extends Bird {
@Override
public void move() {
System.out.println("Penguin waddles");
}
}
Hierarchical Class Design
Multi-Level Inheritance
public class HierarchicalInheritance {
abstract static class Vehicle {
protected String brand;
abstract void start();
}
abstract static class Car extends Vehicle {
protected int numDoors;
abstract void honk();
}
static class Sedan extends Car {
private boolean isAutomatic;
public Sedan(String brand, boolean isAutomatic) {
this.brand = brand;
this.numDoors = 4;
this.isAutomatic = isAutomatic;
}
@Override
void start() {
System.out.println(brand + " sedan starting");
}
@Override
void honk() {
System.out.println("Horn: Beep beep");
}
}
}
Template Method Pattern
public class TemplateMethod {
abstract static class DataProcessor {
// Template method - defines algorithm structure
public final void processData(String input) {
String validated = validate(input);
String transformed = transform(validated);
String enriched = enrich(transformed);
display(enriched);
}
// These must be implemented by subclasses
protected abstract String validate(String input);
protected abstract String transform(String input);
protected abstract String enrich(String input);
// Common implementation
protected void display(String output) {
System.out.println("Result: " + output);
}
}
static class CSVProcessor extends DataProcessor {
@Override
protected String validate(String input) {
return input.trim();
}
@Override
protected String transform(String input) {
return input.replace(",", " | ");
}
@Override
protected String enrich(String input) {
return "[CSV] " + input;
}
}
static class JSONProcessor extends DataProcessor {
@Override
protected String validate(String input) {
return input.trim();
}
@Override
protected String transform(String input) {
return "{\"data\": \"" + input + "\"}";
}
@Override
protected String enrich(String input) {
return input; // Already in JSON
}
}
}
Method Overriding Best Practices
Respecting Liskov Substitution Principle
public class LSPDemo {
abstract static class Shape {
abstract double getArea();
}
static class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double getArea() {
return width * height;
}
}
static class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
double getArea() {
return Math.PI radius radius;
}
}
// Can use any Shape subclass interchangeably
static void printShapeArea(Shape shape) {
System.out.println("Area: " + shape.getArea());
}
}
Composition vs Inheritance
public class CompositionVsInheritance {
// Problem with inheritance: rigid hierarchy
static class Dog extends Animal {
void bark() { }
}
// Better solution: composition for flexibility
interface Behavior {
void perform();
}
static class BarkingBehavior implements Behavior {
public void perform() {
System.out.println("Woof!");
}
}
static class AnimalComposed {
private Behavior behavior;
public AnimalComposed(Behavior behavior) {
this.behavior = behavior;
}
public void act() {
behavior.perform();
}
}
}
Inheritance Patterns and Anti-Patterns
Good Pattern: Single Responsibility
abstract static class DatabaseConnection {
abstract void connect();
abstract void query(String sql);
}static class MySQLConnection extends DatabaseConnection {
@Override
void connect() { System.out.println("MySQL connect"); }
@Override
void query(String sql) { System.out.println("MySQL query"); }
}
static class PostgreSQLConnection extends DatabaseConnection {
@Override
void connect() { System.out.println("PostgreSQL connect"); }
@Override
void query(String sql) { System.out.println("PostgreSQL query"); }
}
Anti-Pattern: Fragile Base Class
static class BaseClass {
protected void process() {
preprocessData();
doProcess(); // If subclass overrides this, bugs may occur
postprocessData();
}
protected void preprocessData() { }
protected void doProcess() { }
protected void postprocessData() { }
}// Subclass breaks parent's assumptions
static class BrokenSubclass extends BaseClass {
@Override
protected void doProcess() {
// Forgot to call methods or changed timing
doProcess();
}
}
Protected Access Gotchas
public class ProtectedAccess {
public static class Parent {
protected String protectedField = "parent";
protected void protectedMethod() {
System.out.println("Parent method");
}
}
public static class Child extends Parent {
public void demonstrateAccess() {
// Can access parent's protected members
System.out.println(protectedField);
protectedMethod();
}
}
public static void main(String[] args) {
Parent parent = new Parent();
// Cannot access protected from outside
// parent.protectedField; // ERROR
// parent.protectedMethod(); // ERROR
// But can access through subclass
Child child = new Child();
child.demonstrateAccess(); // Works
}
}
Key Takeaways
1. extends creates parent-child relationships
2. Subclasses inherit all public and protected members
3. Override methods to specialize behavior
4. super() calls parent constructor; super.method() calls parent method
5. Abstract classes define templates; subclasses must implement abstract methods
6. protected access allows subclass access while hiding implementation details
7. Inheritance represents IS-A relationships
8. Subclasses should work everywhere parent classes do (Liskov Substitution)
9. Composition often provides more flexibility than inheritance
10. Use inheritance for is-a relationships; composition for has-a relationships
11. Template method pattern uses inheritance for algorithmic structure
12. Be careful with protected access—it binds subclasses to implementation details
Quiz
Question 1: What happens if you don't call super() in a subclass constructor?
- A) Java automatically calls it ✓
- B) Compilation error
- C) Runtime error
- D) The parent is not initialized
Question 2: Can you instantiate an abstract class?
- A) Yes, always
- B) Yes, if it has implemented methods
- C) No, never ✓
- D) Only through subclasses
Question 3: What's the difference between private and protected?
- A) Same thing
- B)
protectedallows subclass access ✓
- C)
privateallows subclass access
- D)
protectedis slower
Question 4: What does super.methodName() do?
- A) Calls a method on the current object
- B) Calls parent class implementation ✓
- C) Creates a new super object
- D) Calls a static method
Question 5: Which statement is true about method overriding?
- A) Return type can change
- B) Parameter list can change
- C) The method signature must match parent ✓
- D) You can override private methods
Question 6: What's the IS-A relationship?
- A) Inheritance relationship ✓
- B) Composition relationship
- C) A method overriding
- D) Object instantiation