Welcome to another core pillar of Object-Oriented Programming (OOP): Inheritance! This powerful concept allows you to define a new class based on an existing class, establishing a hierarchical relationship. Inheritance is all about reusability and creating a logical "is-a" relationship between classes.

Imagine you're designing a system for different types of vehicles. All vehicles have some common properties (like speed, color) and behaviors (like accelerating, braking). Instead of redefining these common elements for Car, Bicycle, Motorcycle, etc., inheritance allows you to create a general Vehicle class and then have Car, Bicycle, and Motorcycle "inherit" those common features.

1. The "Is-A" Relationship

Inheritance models the "is-a" relationship.

  • A Car is a Vehicle.

  • A Dog is an Animal.

  • A Student is a Person.

This relationship is fundamental to understanding when to use inheritance. If a class A "is a" type of class B, then A can inherit from B.

2. Parent Class (Superclass) and Child Class (Subclass)

In an inheritance relationship:

  • Parent Class / Superclass / Base Class: The existing class whose features are being inherited. (e.g., Vehicle, Animal, Person)

  • Child Class / Subclass / Derived Class: The new class that inherits features from the parent class. (e.g., Car, Dog, Student)

A subclass inherits all the non-private members (fields and methods) of its superclass. This means you don't have to rewrite the common code.

3. The extends Keyword

In Java, you use the extends keyword to establish an inheritance relationship.

Syntax:

class Subclass extends Superclass {
    // Subclass specific fields and methods
}

Example:

// Superclass / Parent Class
class Animal {
    String species;
    int age;

    public Animal(String species, int age) {
        this.species = species;
        this.age = age;
        System.out.println("Animal constructor called.");
    }

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

    public void sleep() {
        System.out.println(species + " is sleeping.");
    }
}

// Subclass / Child Class
class Dog extends Animal {
    String breed;

    public Dog(String breed, int age) {
        // Call the superclass constructor using 'super()'
        super("Dog", age); // Calls Animal's constructor
        this.breed = breed;
        System.out.println("Dog constructor called.");
    }

    // Dog's specific method
    public void bark() {
        System.out.println("Woof woof!");
    }

    public static void main(String[] args) {
        Dog myDog = new Dog("Golden Retriever", 3);
        System.out.println("My dog's species: " + myDog.species); // Inherited field
        System.out.println("My dog's age: " + myDog.age);       // Inherited field
        System.out.println("My dog's breed: " + myDog.breed);     // Dog's own field

        myDog.eat();   // Inherited method from Animal
        myDog.sleep(); // Inherited method from Animal
        myDog.bark();  // Dog's own method
    }
}

Explanation:

  • The Dog class extends the Animal class. This means Dog inherits species, age, eat(), and sleep() from Animal.

  • The Dog class also has its own unique field (breed) and method (bark()).

  • In the Dog constructor, super("Dog", age) is used to call the constructor of the Animal superclass. This is crucial for initializing the inherited parts of the Dog object.

4. Benefits of Inheritance

  1. Code Reusability: The most significant benefit. You write common code once in the superclass, and all subclasses automatically get access to it. This saves development time and reduces errors.

  2. Polymorphism: Inheritance is a prerequisite for polymorphism, allowing objects of different classes to be treated as objects of a common superclass. (More on this in a future lesson!)

  3. Logical Hierarchy: Helps organize classes in a logical, tree-like structure, making your code easier to understand and manage.

  4. Maintainability: Changes to common functionality in the superclass automatically propagate to all subclasses, simplifying updates.

5. What is Inherited (and What Isn't)?

  • What is Inherited?

    • Public fields and methods: These are fully accessible and usable by the subclass.

    • Protected fields and methods: These are accessible within the same package AND by subclasses, even if the subclass is in a different package.

  • What is NOT Directly Inherited?

    • Private members: Private fields and methods of the superclass are NOT directly accessible by the subclass. However, they can be accessed indirectly if the superclass provides public or protected getter/setter methods for them (which is a good practice, linking back to encapsulation!).

    • Constructors: Constructors are special methods used to initialize objects, and they are NOT inherited. Each class must define its own constructors. However, a subclass's constructor must call a superclass constructor (explicitly or implicitly).

6. Method Overriding

Method overriding occurs 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 signature (name, return type, and parameters) as the method in the superclass.

  • @Override Annotation: It's good practice (and highly recommended) to use the @Override annotation above a method that you intend to override. This helps the compiler check if you've correctly overridden the method, catching typos or signature mismatches.

Example:

class Vehicle {
    String brand;

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

    public void start() {
        System.out.println(brand + " vehicle is starting.");
    }

    public void stop() {
        System.out.println(brand + " vehicle is stopping.");
    }
}

class Car extends Vehicle {
    int numberOfDoors;

    public Car(String brand, int numberOfDoors) {
        super(brand); // Call Vehicle's constructor
        this.numberOfDoors = numberOfDoors;
    }

    @Override // Good practice: indicates this method overrides a superclass method
    public void start() {
        System.out.println(brand + " car is starting with a key.");
    }

    // Car-specific method
    public void drive() {
        System.out.println(brand + " car is driving.");
    }

    public static void main(String[] args) {
        Vehicle genericVehicle = new Vehicle("Generic Motors");
        genericVehicle.start(); // Output: Generic Motors vehicle is starting.

        Car myCar = new Car("Toyota", 4);
        myCar.start();      // Output: Toyota car is starting with a key. (Overridden method)
        myCar.stop();       // Output: Toyota vehicle is stopping. (Inherited method)
        myCar.drive();      // Output: Toyota car is driving. (Car's own method)
    }
}

Explanation:

  • Both Vehicle and Car have a start() method.

  • Car's start() method is marked with @Override and provides a more specific implementation for how a Car starts.

  • When myCar.start() is called, the Car class's version of start() is executed. When genericVehicle.start() is called, Vehicle's version is executed.

7. The super Keyword

The super keyword in Java is used to refer to the superclass (parent class) of a subclass. It has two main uses:

  1. Calling Superclass Constructor:

    • super() is used to invoke the constructor of the immediate superclass.

    • This must be the first statement in the subclass's constructor.

    • If you don't explicitly call super(), Java's compiler will automatically insert a call to the superclass's no-argument constructor (super();) if one exists. If the superclass only has parameterized constructors, you must call super() explicitly with the correct parameters.

  2. Calling Superclass Methods:

    • super.methodName() is used to call a method from the superclass, even if that method has been overridden in the current subclass. This allows you to extend or augment the superclass's behavior.

Example:

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person constructor called.");
    }

    public void introduce() {
        System.out.println("Hi, my name is " + name + " and I am " + age + " years old.");
    }
}

class Employee extends Person {
    String employeeId;
    double salary;

    public Employee(String name, int age, String employeeId, double salary) {
        super(name, age); // Call to Person's constructor - MUST BE THE FIRST STATEMENT
        this.employeeId = employeeId;
        this.salary = salary;
        System.out.println("Employee constructor called.");
    }

    @Override
    public void introduce() {
        super.introduce(); // Call the introduce() method from the Person superclass
        System.out.println("I am an employee with ID: " + employeeId + " and earn $" + salary);
    }

    public void work() {
        System.out.println(name + " is working.");
    }

    public static void main(String[] args) {
        Employee emp = new Employee("Jane Doe", 30, "EMP123", 75000.00);
        emp.introduce(); // This calls the overridden introduce() in Employee
                         // which in turn calls the Person's introduce()
        emp.work();
    }
}

Explanation:

  • In Employee's constructor, super(name, age) ensures that the name and age fields (inherited from Person) are initialized correctly by calling Person's constructor.

  • In Employee's introduce() method, super.introduce() calls the introduce() method defined in the Person class, allowing the employee to first introduce themselves as a person, and then add their employee-specific details.

8. Single Inheritance in Java

Java classes support single inheritance only. This means a class can extend only one direct superclass.

// Valid
class Dog extends Animal {}

// Invalid - Will cause a compile-time error
// class HybridCar extends ElectricCar, GasolineCar {}

While a class can only extend one other class, an inheritance hierarchy can be very deep (e.g., Object -> Animal -> Dog -> GoldenRetriever). Java achieves similar concepts to multiple inheritance through interfaces (which will be covered in a later lesson).

9. The final Keyword with Inheritance (Brief Mention)

The final keyword can impact inheritance:

  • final class: A class declared final cannot be extended by any other class. (e.g., String class).

  • final method: A method declared final in a superclass cannot be overridden in any subclass.

Key Takeaways

  • Inheritance establishes an "is-a" relationship between classes.

  • The extends keyword is used to create a subclass (child) from a superclass (parent).

  • Subclasses inherit public and protected members from their superclass.

  • Private members and constructors are NOT inherited (though constructors must be called using super()).

  • Method Overriding allows a subclass to provide its own specific implementation of an inherited method, often indicated by the @Override annotation.

  • The super keyword is used to call the superclass's constructor (as the first line in a subclass constructor) or to invoke a superclass method.

  • Java supports single inheritance for classes (a class can only extend one other class).

  • final classes cannot be extended, and final methods cannot be overridden.

Exercise

To solidify your understanding of inheritance, try the following exercise:

Problem: Design a simple hierarchy for Shapes.

  1. Create a Superclass Shape:

    • It should have private instance variables for color (String) and isFilled (boolean).

    • A constructor that takes color and isFilled.

    • Public getter methods for color and isFilled.

    • A public method getArea() that returns a double. For the Shape class itself, this method should return 0.0 or throw an UnsupportedOperationException, as a generic shape doesn't have a concrete area.

    • A public method getPerimeter() that also returns 0.0 or throws an UnsupportedOperationException.

    • A public method displayInfo() that prints the shape's color and whether it's filled.

  2. Create a Subclass Circle that extends Shape:

    • It should have a private instance variable for radius (double).

    • A constructor that takes color, isFilled, and radius. Remember to use super() to call the Shape constructor.

    • Public getter and setter methods for radius. Ensure radius is positive in the setter.

    • Override the getArea() method to calculate the area of a circle (πr2). (You can use Math.PI and Math.pow(radius, 2)).

    • Override the getPerimeter() method to calculate the circumference of a circle (2πr).

    • Override displayInfo() to call super.displayInfo() and then add the circle's radius, area, and perimeter.

  3. Create a Subclass Rectangle that extends Shape:

    • It should have private instance variables for width (double) and height (double).

    • A constructor that takes color, isFilled, width, and height. Remember to use super().

    • Public getter and setter methods for width and height. Ensure both are positive in their setters.

    • Override the getArea() method to calculate the area of a rectangle (width * height).

    • Override the getPerimeter() method to calculate the perimeter of a rectangle (2×(width+height)).

    • Override displayInfo() to call super.displayInfo() and then add the rectangle's width, height, area, and perimeter.

  4. In a main method (in any of the classes or a separate test class):

    • Create objects of Circle and Rectangle.

    • Call displayInfo() for each object.

    • Try to use setters to modify dimensions and observe the new calculated area/perimeter after calling displayInfo() again.

    • (Optional but good practice): Try to create a Shape object directly and call getArea() or getPerimeter() to see the UnsupportedOperationException (if you implemented it that way).

This exercise will give you practical experience with defining inheritance hierarchies, calling superclass constructors, overriding methods, and using encapsulated data

Key Takeaways

  • The primary benefit is writing common code once in a superclass (parent) and having subclasses (children) automatically inherit those features, reducing redundancy.

  • Subclasses inherit all public and protected members (fields and methods) from their superclass.

    • private members of the superclass are not directly accessible by subclasses (though they can be accessed via public/protected getters/setters).

    • Constructors are not inherited; each class must define its own constructors.

Quiz