Hi! You already use Java methods and know a lot about them.

Surely you have come across a class with many methods that have the same name but different argument lists. You will recall that in those cases we used method overloading.

Today we'll take a look at a different situation. Imagine that we have one common method, but it should do different things depending on which class it is called in.

How do we implement this behavior?

To understand this, let's take the Animal parent class, which represents animals, and create a speak method in it:

public class Animal {

   public void speak() {

       System.out.println("Hello!");
   }
}

Although we just started writing our program, you can probably see a potential problem: the world is full of lots of animals, and they all "speak" differently: cats meow, ducks quack, snakes hiss, etc.

Our goal is simple: we want to avoid creating a bunch of methods for speaking. Instead of creating a meow() method for meowing, hiss() for hissing, etc., we want the snake to hiss, the cat to meow, and the dog to bark when the speak() method is called.

We can easily achieve this using method overriding.

Wikipedia explains the term as follows: Method overriding, in object-oriented programming, is a language feature that allows a subclass or child class to provide a specific implementation of a method that is already provided by one of its superclasses or parent classes.

That's basically correct. Overriding lets you take some method of a parent class and write your own implementation in each derived class. The new implementation in the child class "replaces" the one in the parent.

Let's see how this looks like with an example. Let's create 4 descendants of our Animal class:

public class Bear extends Animal {
   @Override
   public void speak() {
       System.out.println("Growl!");
   }
}
public class Cat extends Animal {

   @Override
   public void speak() {
       System.out.println("Meow!");
   }
}

public class Dog extends Animal {

   @Override
   public void speak() {
       System.out.println("Woof!");
   }
}


public class Snake extends Animal {

   @Override
   public void speak() {
       System.out.println("Hiss!");
   }
}

Here's a small lifehack for the future: to override the methods of a parent class, go into the code of the derived class in IntelliJ IDE, press Ctrl+O, and select Override methods... from the menu. Get used to using hotkeys from the outset. They will speed up coding!

To get the desired behavior, we did a few things:

  1. In each descendant class, we created a method with the same name as the method in the parent class.

  2. We told the compiler that we aren't just giving the method the same name as in the parent class but rather we want to override its behavior. This "message" to the compiler is conveyed via the @Override annotation.
    The @Override annotation above a method tells the compiler (as well as other programmers reading your code), "Don't worry. This is not a mistake or oversight. I am aware that this method already exists, and I want to override it.

  3. We wrote the implementation we need for each descendant class. When the speak() method is called, a snake should hiss, a bear should growl, and so on.

Let's see how this works in a program:

public class Main {

   public static void main(String[] args) {

       Animal animal1 = new Dog();
       Animal animal2 = new Cat();
       Animal animal3 = new Bear();
       Animal animal4 = new Snake();

       animal1.speak();
       animal2.speak();
       animal3.speak();
       animal4.speak();
   }
}

Console output:


Woof! 
Meow! 
Growl! 
Hiss!

Great, everything works as it should! We created 4 reference variables whose type is the Animal parent class, and assigned them 4 different objects of the descendant classes.

As a result, each object behaves differently. For each of the derived classes, the overridden speak() method replaces the existing speak() method of the Animal class (which simply displays "Speaking: " on the console).

Method overriding has several limitations:

  1. An overridden method must have the same arguments as the method in the parent class.

    If the speak method of the parent class takes a String as input, then the overridden method in the descendant class must also take a String as input. Otherwise, the compiler will generate an error:

    public class Animal {
    
       public void speak(String s) {
    
           System.out.println("Speaking: " + s);
       }
    }
    
    public class Cat extends Animal {
    
       @Override // Error!
       public void speak() {
           System.out.println("Meow!");
       }
    }

  2. The overridden method must have the same return type as the method in the parent class.

    Otherwise, we will get a compilation error:

    public class Animal {
    
       public void speak() {
    
           System.out.println("Hello!");
       }
    }
    
    
    public class Cat extends Animal {
    
       @Override
       public String speak() {         // Error!
           System.out.println("Meow!");
           return "Meow!";
       }
    }

  3. The overridden method's access modifier also cannot differ from the original one:

    public class Animal {
    
       public void speak() {
    
           System.out.println("Hello!");
       }
    }
    
    public class Cat extends Animal {
    
       @Override
       private void speak() {      // Error!
           System.out.println("Meow!");
       }
    }

In Java, method overriding is one way to implement polymorphism.

That means that its main advantage is that flexibility that we talked about earlier. We can build a simple and logical hierarchy of classes, each with specific behavior (barking dogs, meowing cats) but a single interface — a single speak() method for everyone rather than a bunch of different methods, e.g. bark(), meow(), etc.