Inheritance in Java – Sharing and Surprising Behaviors

Introduction

Inheritance is often touted as the heart of OOP: a way to share code, model hierarchies, and reuse behavior. In Java, inheritance brings its own set of rules and quirks, especially when you’re guiding students through it for the first time. Let’s dive into Java’s inheritance model, explore some teaching moments, and uncover the surprises that only show up when you’re really using it.


1. The Basics: extends and the Class Hierarchy

Every Java class (except Object) extends another class:

public class Animal {
    public void eat() {
        System.out.println("Nom nom");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("Woof!");
    }
}
  • Dog inherits eat() from Animal.
  • The root of all classes is java.lang.Object, so even if you don’t write extends, your class is silently extending Object.
public class MyClass { }
// is actually:
public class MyClass extends Object { }

Teaching Tip:

Show students how every class has methods like toString(), hashCode(), and equals() because they come from Object.


2. Constructor Chaining and super()

When you new up a subclass, Java ensures the superclass is initialized first:

public class Person {
    public Person(String name) {
        System.out.println("Person created: " + name);
    }
}

public class Employee extends Person {
    public Employee(String name, String id) {
        super(name);                    // mandatory call to super
        System.out.println("Employee ID: " + id);
    }
}
  • If you don’t call super(...) explicitly, Java inserts a no-arg super(), but only if the superclass has a no-arg constructor.
  • Missing or mismatched constructors lead to confusing errors.

Teaching Tip:

Demonstrate the compile-time error when a superclass lacks a matching no-arg constructor, then show how adding super(args) resolves it.


3. Method Overriding and Polymorphism

Subclasses can override methods to provide specialized behavior:

@Override
public void eat() {
    System.out.println("Crunch crunch");
}

With a reference of the parent type, overridden methods still dispatch to the subclass:

Animal a = new Dog();
a.eat();      // Crunch crunch

Quirk:

Java disallows overriding methods marked final. And private methods are invisible to subclasses, not overridden but hidden.


4. Single Inheritance vs. Multiple Interfaces

Java classes can extend only one parent class (single inheritance), but they can implement multiple interfaces:

public interface Swimmer { void swim(); }
public interface Runner { void run(); }

public class Triathlete implements Swimmer, Runner {
    public void swim() { }
    public void run()  { }
}

Java 8 Default Methods and the Diamond

Since Java 8, interfaces can have default methods:

public interface Flyer {
    default void move() { System.out.println("Flap flap"); }
}
public interface Walker {
    default void move() { System.out.println("Step step"); }
}

public class Duck implements Flyer, Walker {
    // Must disambiguate:
    public void move() {
        Flyer.super.move(); // or Walker.super.move();
    }
}
  • If two interfaces provide the same default method, the implementing class must override it, otherwise it won’t compile.

Teaching Tip:

Walk through a “diamond inheritance” scenario to show why Java forces explicit disambiguation.


5. Visibility and the protected Gotcha

  • public: visible everywhere.
  • private: visible only in the declaring class.
  • protected: visible in the same package and in subclasses (even if in different packages).
  • (default/package-private): visible only within the same package.

Quirk:

Students often assume protected means “only subclasses,” but it’s broader: package-level code without subclass ties can also see it if they’re in the same package.


6. final Classes and Methods

  • A final class cannot be subclassed (e.g., String).
  • A final method cannot be overridden.
  • Useful when you want to lock down behavior or prevent unsafe subclassing.

7. Covariant Return Types

Since Java 5, overridden methods can return a subtype of the original return type:

class Fruit {}
class Apple extends Fruit {}

class Orchard {
    Fruit pick() { return new Fruit(); }
}

class AppleOrchard extends Orchard {
    @Override
    Apple pick() { return new Apple(); }  // legal
}

Teaching Tip:

Explain how covariant returns improve API usability without compromising type safety.


Key Takeaways

  1. Java’s single inheritance model is complemented by multiple interfaces.
  2. Constructor chaining always travels up to Object.
  3. Default methods introduced interface inheritance quirks.
  4. Visibility rules (especially protected) can surprise new Java developers.
  5. final and covariant returns fine-tune how and whether subclasses can change behavior.

In the next post, we’ll tackle polymorphism in action, dynamic dispatch, abstract classes, and when to choose interfaces vs. abstract base classes. Stay tuned!