Week 2

Inheritance

Interfaces

Polymorphism

Error Handling in Java

Practice

Assignment

Back end Track

🎯 Learning Objectives

By the end of this module, you will be able to:


1. What is Inheritance?

Inheritance is a mechanism that lets one class acquire the fields and methods of another. It models an "is-a" relationship — if you can truthfully say "A is a B", inheritance is likely the right tool.

Some examples:

The last example is important. "Has-a" relationships should use composition (a field), not inheritance. Choosing the wrong one is one of the most common OOP design mistakes.

🔍 You saw inheritance in JavaScript with class Dog extends Animal. Java works the same way conceptually — the syntax is nearly identical, but Java enforces stricter rules around types and access that make the structure more explicit.

Without inheritance, you'd have to repeat shared fields and methods in every class:

// Without inheritance — duplicated code
public class Dog {
    String name;
    int age;
    public void eat() { System.out.println("Eating..."); }
    public void bark() { System.out.println("Woof!"); }
}

public class Cat {
    String name; // duplicated
    int age;     // duplicated
    public void eat() { System.out.println("Eating..."); } // duplicated
    public void meow() { System.out.println("Meow!"); }
}

With inheritance, shared code lives in one place:

// With inheritance — shared code defined once
public class Animal {
    String name;
    int age;
    public void eat() { System.out.println("Eating..."); }
}

public class Dog extends Animal {
    public void bark() { System.out.println("Woof!"); }
}

public class Cat extends Animal {
    public void meow() { System.out.println("Meow!"); }
}

2. The extends Keyword

Use extends to declare that one class inherits from another:

public class SubClass extends SuperClass {
    // SubClass now has everything SuperClass has, plus its own additions
}

The class being extended is called the superclass (parent). The class doing the extending is called the subclass (child).

public class Animal {           // superclass
    String name;
    int age;

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

public class Dog extends Animal {   // subclass
    public void bark() {
        System.out.println(name + " says: Woof!"); // name inherited from Animal
    }
}
Dog dog = new Dog();
dog.name = "Rex";   // inherited field
dog.age  = 3;       // inherited field
dog.eat();          // inherited method → Rex is eating.
dog.bark();         // own method → Rex says: Woof!

⚠️ Java supports single inheritance only — a class can extend at most one other class. This is different from some other languages (like C++) that allow multiple inheritance. Java addresses this limitation with interfaces, which you'll cover in the next module.


3. What is Inherited (and What is Not)

Not everything from the superclass is automatically available in the subclass:

Member Inherited? Notes
public fields ✅ Yes Directly accessible
protected fields ✅ Yes Accessible in subclass
public methods ✅ Yes Can be called and overridden
protected methods ✅ Yes Can be called and overridden
private fields ❌ No Exist in memory but not directly accessible — use getters
private methods ❌ No Not accessible in subclass
Constructors ❌ No Must be defined separately in each class
public class Animal {
    private String name;   // private — not directly accessible in subclass
    protected int age;     // protected — accessible in subclass

    public String getName() { return name; }   // public getter — inherited ✅
    public void setName(String name) { this.name = name; }
}

public class Dog extends Animal {
    public void describe() {
        // System.out.println(name);      // ❌ private — not accessible
        System.out.println(getName());    // ✅ use the inherited getter
        System.out.println(age);         // ✅ protected — directly accessible
    }
}

💡 This is why encapsulation and inheritance work together — making fields private and exposing them through public/protected methods keeps the superclass in control of its own data even as subclasses extend it.


4. The super Keyword — Calling the Parent Constructor

Since constructors are not inherited, each subclass must define its own. But often a subclass constructor needs to initialise the fields defined in the superclass. The super() call delegates this to the parent constructor.

public class Animal {
    private String name;
    private int    age;

    public Animal(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    public String getName() { return name; }
    public int    getAge()  { return age; }
}

public class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);       // calls Animal(String, int) — must be first line
        this.breed = breed;
    }

    public String getBreed() { return breed; }
}
Dog dog = new Dog("Rex", 3, "Labrador");
System.out.println(dog.getName());  // Rex
System.out.println(dog.getAge());   // 3
System.out.println(dog.getBreed()); // Labrador

Rules for super():

🔍 In JavaScript, you also call super() at the top of a subclass constructor before using this. Java enforces the same rule — the difference is Java will refuse to compile if you forget.


5. super for Calling Parent Methods

super isn't only for constructors. You can also use it to call a method from the superclass when you've overridden it in the subclass and still want to use the parent's version:

public class Animal {
    public void describe() {
        System.out.println("I am an animal.");
    }
}

public class Dog extends Animal {
    @Override
    public void describe() {
        super.describe();                   // calls Animal's describe()
        System.out.println("I am a dog."); // adds Dog-specific behaviour
    }
}
Dog dog = new Dog();
dog.describe();
// I am an animal.
// I am a dog.

This is useful when you want to extend the parent's behaviour rather than completely replace it.


6. Method Overriding

Overriding means redefining a method from the superclass in the subclass with the same signature (same name, same parameters, same return type) but different behaviour.

public class Animal {
    public String makeSound() {
        return "Some generic sound";
    }
}

public class Dog extends Animal {
    @Override
    public String makeSound() {
        return "Woof!";
    }
}

public class Cat extends Animal {
    @Override
    public String makeSound() {
        return "Meow!";
    }
}
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();

System.out.println(a.makeSound()); // Some generic sound
System.out.println(d.makeSound()); // Woof!
System.out.println(c.makeSound()); // Meow!

Notice that d and c are declared as type Animal, but they run the Dog and Cat versions of makeSound(). This is polymorphism — the ability to treat different objects through a common type, and have each respond in its own way. You'll go deeper into this in the next module.

Rules for overriding:


7. The @Override Annotation

@Override is placed above a method to tell the compiler: "I intend this method to override one from the superclass."

@Override
public String makeSound() {
    return "Woof!";
}

It is not required — the override happens with or without it. But it gives you two important benefits:

  1. Compile-time safety — if the signature doesn't match any parent method (e.g. you made a typo), the compiler throws an error immediately. Without @Override, Java would silently create a new method instead of overriding, which is a very hard bug to spot.
  2. Readability — it signals to anyone reading the code that this method has a parent version.
// Without @Override — typo goes undetected
public String makesound() {   // lowercase 's' — not an override, a new method
    return "Woof!";
}

// With @Override — compiler catches the typo immediately
@Override
public String makesound() {   // ❌ Compile error: method does not override
    return "Woof!";
}

💡 Always use @Override. It costs nothing and prevents a category of bugs that are otherwise very difficult to diagnose.


8. Method Overloading vs Method Overriding

These two concepts sound similar but are completely different:

| --- | --- | --- |

// Overloading — same class, different parameters
public class Calculator {
    public int add(int a, int b)          { return a + b; }         // version 1
    public double add(double a, double b) { return a + b; }         // version 2
    public int add(int a, int b, int c)   { return a + b + c; }     // version 3
}

// Overriding — subclass, identical signature
public class Animal {
    public String makeSound() { return "..."; }
}

public class Dog extends Animal {
    @Override
    public String makeSound() { return "Woof!"; }   // replaces parent's version
}

A useful memory aid: overloading = more options in the same class. Overriding = replacing in a subclass.


9. The Object Class

Every class in Java — including every class you have written so far — implicitly extends Object. It is the root of the entire Java class hierarchy.

public class Animal { ... }

// is exactly equivalent to:
public class Animal extends Object { ... }

Because every class inherits from Object, every object in Java automatically has these methods:

| --- | --- |

You've already used toString() — when you call System.out.println(myObject), Java automatically calls myObject.toString().

The default implementations are rarely useful, which is why you override them:

public class Animal {
    private String name;
    private int    age;

    public Animal(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    // Default Object.toString() would print: Animal@1b6d3586
    // Our override prints something meaningful:
    @Override
    public String toString() {
        return "Animal{name='" + name + "', age=" + age + "}";
    }

    // Default Object.equals() checks reference equality (same as ==)
    // Our override checks field equality:
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Animal)) return false;
        Animal other = (Animal) o;
        return age == other.age && name.equals(other.name);
    }
}
Animal a1 = new Animal("Rex", 3);
Animal a2 = new Animal("Rex", 3);

System.out.println(a1);            // Animal{name='Rex', age=3}
System.out.println(a1.equals(a2)); // true — same content
System.out.println(a1 == a2);      // false — different objects in memory

💡 equals() and hashCode() always go together — if you override one, you should override the other. You'll explore why in depth when you cover Collections in a later week. For now, focus on toString() and understanding the role of Object.


✏️ Exercises

Exercise 1 — Is-a or Has-a?

For each pair, decide whether the relationship is "is-a" (inheritance) or "has-a" (composition). Justify each answer in one sentence.

  1. ElectricCar and Car
  2. Car and Engine
  3. Manager and Employee
  4. Student and Address
  5. Laptop and Device
  6. Order and Product

Abstract Classes