Course outline · 0% complete

0/29 lessons0%

Course overview →

Overriding and polymorphism

lesson 6-2 · ~7 min · 17/29

Overriding

Inheriting a method as it stands is only half the story. A SavingsAccount needs withdraw to behave differently from its parent rather than identically.

A subclass can replace an inherited method by redefining it with the same signature, which is called overriding. It is a different mechanism from overloading in lesson 4-2, which was the same name with different parameters:

MechanismWhat differsChosen
overloadingthe parameter listat compile time
overridingthe class providing the bodyat runtime
class Animal {
  void speak() { System.out.println("..."); }
}

class Dog extends Animal {
  @Override
  void speak() { System.out.println("woof"); }
}

Now the payoff, polymorphism. A variable of type Animal may hold any animal, and Java picks the method of the object's actual class at runtime:

Animal pet = new Dog();
pet.speak();   // woof, not ...

One loop over an Animal[] can therefore make every species speak correctly without a single if statement, and adding a new species requires no change to that loop.

One loop, three voices

The array is typed Animal[], yet each element speaks for itself.

public class Main {
  public static void main(String[] args) {
    Animal[] pets = { new Dog(), new Cat(), new Animal() };
    for (Animal pet : pets) {
      pet.speak();
    }
  }
}

class Animal {
  void speak() {
    System.out.println("...");
  }
}

class Dog extends Animal {
  @Override
  void speak() {
    System.out.println("woof");
  }
}

class Cat extends Animal {
  @Override
  void speak() {
    System.out.println("meow");
  }
}

Output

woof
meow
...

The loop body is one line and contains no type test. Each call finds the override belonging to the object it landed on, which is dispatch by actual class.

Adding a Cow class with its own speak and dropping it into the array would print moo without touching main at all. That property, new behavior without editing existing code, is the reason polymorphism matters in large systems.

Animalspeak() → "..."Dogspeak() → "woof"Catspeak() → "meow"Animal pet = new Dog(); pet.speak() runs Dog's version
Subclasses point up at their superclass. A call through an Animal variable runs the override of the object's actual class.

Which override runs, and when it is chosen

For Animal pet = new Cat(); followed by pet.speak();, Cat's version runs, and the choice is made at runtime from the object's actual class.

The variable's declared type, Animal, controls what you may call. The object's actual class, Cat, controls which body runs. Java looks the method up while the program is running, and that late decision is called dynamic dispatch.

QuestionAnswered by
which methods can I callthe declared type, at compile time
which version executesthe actual class, at runtime

The split explains an apparent contradiction. pet.speak() runs Cat's code, yet pet.purr() does not compile even though the object is a Cat, because the compiler only trusts the declared type.

Asking what an object really is

A variable typed Animal exposes only Animal's methods. Even when the object inside is a Dog, pet.fetch() will not compile, because the compiler can trust nothing beyond the declared type.

When you genuinely need the specific type back, instanceof tests what the object actually is, and its pattern form hands you a correctly typed variable in one step:

Animal pet = new Dog();
pet instanceof Dog          // true, it asks about the object

if (pet instanceof Dog d) { // test and cast in one move
  d.fetch();                // d is a Dog here
}

Use it sparingly. A chain of instanceof checks selecting behavior per type is usually a sign that the behavior belongs in an overridden method instead, which is what polymorphism is for.

The legitimate uses tend to sit at boundaries, where an object arrives from outside your own hierarchy and its type has to be established before anything else can happen.

Recovering the specific type

Two instanceof tests and one pattern match on the same object.

class Animal {
  void speak() { System.out.println("..."); }
}

class Dog extends Animal {
  @Override
  void speak() { System.out.println("woof"); }

  void fetch() { System.out.println("fetching!"); }
}

public class Main {
  public static void main(String[] args) {
    Animal pet = new Dog();
    System.out.println(pet instanceof Dog);
    System.out.println(pet instanceof Animal);

    if (pet instanceof Dog d) {
      d.fetch();
    }
  }
}

Output

true
true
fetching!

Both tests pass, because the object is a Dog and every Dog is also an Animal. The declared type of pet played no part in either answer.

The pattern form declares d only inside the if, and d is Dog-typed there, so d.fetch() compiles. Without that, the call would need an explicit ((Dog) pet).fetch() cast, which throws at runtime if the guess is wrong.

Summing areas without asking the type

Two shapes with different formulas, added up by one loop.

public class Main {
  public static void main(String[] args) {
    Shape[] shapes = { new Square(3), new Rect(2, 4) };
    double total = 0;
    for (Shape s : shapes) {
      total += s.area();
    }
    System.out.println("total area: " + total);
  }
}

class Shape {
  double area() {
    return 0.0;
  }
}

class Square extends Shape {
  double side;

  Square(double side) {
    this.side = side;
  }

  @Override
  double area() {
    return side * side;
  }
}

class Rect extends Shape {
  double width;
  double height;

  Rect(double width, double height) {
    this.width = width;
    this.height = height;
  }

  @Override
  double area() {
    return width * height;
  }
}

Output

total area: 17.0

The square of side 3 contributes 9.0 and the 2 by 4 rectangle contributes 8.0, so the accumulator ends at 17.0. The loop never learns which shape is which, since total += s.area() does the dispatch.

Shape.area() returning 0.0 is the weak point here. It is a placeholder that means nothing, and a subclass that forgot to override it would silently contribute zero, which is exactly the problem the next lesson fixes.