Bits of Java – Episode 15: Method overriding vs method overloading

In this episode we will discuss a bit the difference between method overriding and method overloading. These are two features that Java supports, under certain conditions, and that can be misleading at a first glance.

Let’s start with the simpler one (at least for me), method overloading.

Method Overloading

A member method is said to be an *overloaded* version of another when it has the same name but a different signature. Given the fact that a method signature is defined as the method name and its parameter list, it turns out that overloaded methods are methods with the same name but a different parameter list.


public class MyClass {

    public void myMethod(String message) {
        System.out.println(message);
    }

    /*
    * This is an overloaded version of the previous one, because
    * the method name is the same, but the parameter list is different.
    */ 
    public void myMethod(int number) {
        //do something
    }

    /*
    * This is NOT an overloaded method, because both the method name and
    * the parameter list are the same. This DOES NOT COMPILE!
    */
    public void myMethod(String mex) {
        //do something
    }
}

Keep in mind that this is the important, unavoidable condition that makes two methods overloaded. In addition to that, overloaded methods can differ also by return type, access modifier, optional specifiers, and declared exceptions.


public class MyClass {

    public void myMethod(String message) {
        System.out.println(message);
    }

    /*
    * This is an overloaded version of the previous one, because
    * the method name is the same, but the parameter list is different.
    * In addition to that also the access modifier and the return type
    * are different here.
    */
    protected int myMethod(int number) {
        return number;
    }

    /*
    * This is NOT an overloaded method, because both the method name and
    * the parameter list are the same. It does not matter if the access modifier
    * and the return type are different. The parameter list should be different,
    * otherwise we do not have an overloaded method. This DOES NOT COMPILE!
    */
    private String myMethod(String mex) {
        return mex;
    }
}

An interesting fact about method overloading is that you have to pay attention when using generics. Since generics types are there only before compiling, these two methods are not valid overloaded methods:


public class MyClass {

    public void myMethod(List<String> messages) {
        for(String msg : messages) {
            System.out.println(msg);
        }
    }

    /*
    * DOES NOT COMPILE because after compiling the parameter will look as
    * List<> numbers, and so the one in the previous method, making them
    * not a valid overloading
    */
    public void myMethod(List<Integer> numbers) {
        for(int num : numbers) {
            System.out.println(num);
        }
    }
}

After compiling the parameters of both these methods will look the same, as generics types are removed, meaning that the methods are not overloaded. They are just the same, and so the compiler gives you an error.

Now that we know how to write an overloaded version of a method, what happens when we need to call them? Well, Java follows a certain order to find the “right” version of the method depending on the arguments you are passing to it.


public class MyClass {

    public void myMethod(String message) {
        System.out.println(message);
    }

    protected int myMethod(int number) {
        return number;
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        /*
        * We are passing a String so the version of the method which
        * accepts a String is used here and the result is that "Hi"
        * is printed
        */
        myClass.myMethod("Hi");

        /*
        * We are passing an int so the version of the method which
        * accepts an int is used and the variable result will 
        * be assigned the value of 3
        */
        int result = myClass.myMethod(3);
    }
}

So far so good, right? That was a pretty intuitive example. Indeed the first thing that Java looks for when dealing with overloaded methods is exact matching. If there is a version of the method which takes exactly the type of the argument you are passing, that version will be the one actually called.

If an exact matching is not found, Java looks if there is a version of the method with a broader type with respect to the one you are passing as argument.


public class MyClass {

    public void myMethod(double number) {
        System.out.println("I am a double!");
    }

    protected void myMethod(int number) {
         System.out.println("I am an int!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        /*
        * We are passing an int so the version of the method which
        * accepts an int is used and the variable result will 
        * be assigned the value of 3
        */
        myClass.myMethod(3); //prints "I am an int!"

        /*
        * Here we are passing a float as argument.
        * There is no version of the method which accepts a float, but
        * there is one which accepts a double, which is a broader type 
        * for float, so Java automatically promote your argument to 
        * double and uses that version of the method!
        */
        myClass.myMethod(3.0f); //prints "I am a double!""
    }
}

Remember the concepts of autoboxing when working with the wrapper classes for primitive types? If not, check here!

This is another thing Java does for you, in case neither an exact matching for your argument nor a version with a broader type is found.


public class MyClass {

    public void myMethod(String word) {
        System.out.println("I am a String!");
    }

    protected void myMethod(Integer number) {
         System.out.println("I am an Integer!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        /*
        * We are passing an int. There is neither an exact matching 
        * version of the method nor one with a broader type. Then
        * Java autoboxes the int to an Integer and it finds the 
        * matching version of the method!
        */
        myClass.myMethod(3); //prints "I am an Integer!""
    }
}

If also autoboxing is not an option, then Java looks for overloaded methods which accept varargs of the same type as your argument.


public class MyClass {

    public void myMethod(String word) {
        System.out.println("I am a String!");
    }

    protected void myMethod(int ... number) {
        for(int num : number) {
             System.out.println("I am an array!");
        }
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        /*
        * We are passing an int. There is neither an exact matching 
        * version of the method nor one with a broader type, nor one 
        * with the wrapper class type. Then Java looks for overloaded 
        * methods with varargs of the same type as parameter!         
        */
        myClass.myMethod(3); //prints "I am an array!""
    }
}

We have gone through the order followed by Java in determining the “right” version of the overloaded method for your arguments! An important thing to keep in mind is that Java executes only one of these steps for you, meaning that if for matching your argument type, for instance, both a promotion to a wider type and an autoboxing are required, no actual match would be found!


public class MyClass {

    public void myMethod(String word) {
        System.out.println("I am a String!");
    }

    protected void myMethod(Integer number) {
         System.out.println("I am an Integer!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        /*
        * DOES NOT COMPILE!!
        * We are passing a short. To match the second version of the method
        * we would need first to promote the short to int and then, since 
        * there is no overloaded method which takes an int, we would need to
        * an autoboxing to get the Integer. These are two steps!
        */
        myClass.myMethod((short)3); //DOES NOT COMPILE!!
    }
}

OK, now that we master the concept of method overloading, we are ready to talk about method overriding!

Method Overriding

You can have method overriding when a class extending another class, or implementing an interface, provides an implementation for one of the methods of the parent class or the interface which can be accessed by that class.

Sounds complicated? Let’s go step by step! You are probably all familiar with the fact that in Java we can have classes which directly extend another class, or which directly implement one or more interfaces.

When a class extends another class, all the methods of the parent class which are not marked as private are inherited by the child class. This class can then decide that the implementation of such methods provided by the parent class (if an implementation is actually provided) is exactly what it wanted to have as behavior, or that it wants to give another implementation for that method. Let’s see two examples.


class ParentClass {
    public void printMessage(String name) {
        System.out.println("Hi " + name);
    }
}

public class ChildClass extends ParentClass {
    public static void main(String[] args) {
        ChildClass myClass = new ChildClass();
        myClass.printMessage("Ilenia"); //prints "Hi Ilenia"
    }
}



class ParentClass {
    public void printMessage(String name) {
        System.out.println("Hi " + name);
    }
}

public class ChildClass extends ParentClass {

    public void printMessage(String name) {
        System.out.println("Bye bye " + name)
    }

    public static void main(String[] args) {
        ChildClass myClass = new ChildClass();
        myClass.printMessage("Ilenia"); //prints "Bye bye Ilenia"
    }
}

In the first example ChildClass extends ParentClass, so it inherits the public method printMessage. It does not provide its own implementation of the message, so when we call the method on an instance of ChildClass the implementation provided by its parent class is called, and the corresponding message is printed.

In the second example, instead, ChildClass still inherits the printMessage method from its parent, but this time it decides it wants to give its own version of the implementation, so it overrides the method, providing its own implementation. When we call the method on an instance of ChildClass, then, the overridden version of the method is called, and so the printed message is no "Hi Ilenia", but "Bye bye Ilenia".

A child class is not obliged to provide an overridden version of the methods in the parent class if the parent class itself already provides an implementation for such methods. But, what happens if the parent class is abstract and provides some abstract methods? Well, in this case, if the child class is again an abstract class then it is still not obliged to override the abstract methods of the parent. If, instead, the child class is a concrete class, then it is obliged to override all the abstarct methods of its parent, plus all the abstract methods of any other indirect parent which have not yet been overridden.


abstract class GrandParentClass{
    public abstract void printMessage(String name);
}

abstract class ParentClass extends GrandParentClass{
    //not obliged to override printMessage() because is an abstarct class
}

public class ChildClass extends ParentClass {
    /*
    * obliged to override the abstact method of all its 
    * parents which have not yet been overriden
    */
    public void printMessage(String name) {
        System.out.println("Hi " + name);
    }
}

Interfaces are defined by default as public and abstract, and their methods are, by default, public and abstract, as well. So, a concrete class implementing an interface, needs to override all the abstract methods of the interface.


public abstract interface MyProvider {
    public abstract void doSomething();
}

class MyClass implements MyProvider {
    //obliged to override the absatrct methods of the interface
    public void doSomething() {
        System.out.println("I am doing something!");
    }
}

We have seen when we can and when we have to override a method. Now let’s see how. Indeed, there are some rules for considering a method a legal overridden version of another.

  • An overridden method must have the same signature of the original one (so, same name AND same parameter list);
  • An overridden method cannot have an access modifier more restrictive than the original one;
  • An overridden method cannot declare a checked Exception of a broader type with respect to the ones declared in the original method;
  • An overridden method must have the same return type of the original one or a covariant return type;
  • static methods cannot be overridden (they can be hidden, though, which will be the topic of next week!).

Let’s see at these rules through some more examples.


class ParentClass {
    protected void printMessage(String name) {
        System.out.println("Hi " + name);
    }
}

class ChildClass extends ParentClass {
    /*
    * This is a valid overridden version: same name, same parameter
    * list, more permissive access modifier (from protected to public)
    */
    public void printMessage(String name) {
        System.out.println("Bye bye " + name);
    }
}

class ChildClass2 extends ParentClass {
    /*
    * DOES NOT COMPILE!!
    * This is NOT a valid overridden version: the access modifier here
    * is the default one which is Package-private, which is more
    * restrictive than protected!
    */
    void printMessage(String name) {
        System.out.println("Bye bye " + name);
    }
}

class ChildClass3 extends ParentClass {
    /*
    * This is NOT a valid overridden version: the signature is not the
    * same, since the parameter list is different. This is actually
    * an OVERLOADED version of the parent method, but NOT AN OVERRIDDEN one!
    */
    public void printMessage(int number) {
        System.out.println("Bye bye " + number);
    }
}

class ChildClass4 extends ParentClass {
    /*
    * This is NOT a valid overridden version: it declares a checked Exception
    * and since in the original parent version no Exception is declared
    * this is not allowed! DOES NOT COMPILE!
    */
    public void printMessage(int number) throws IOException {
        System.out.println("Bye bye " + number);
    }
}

To understand the fourth rule about overriding, we need to review what it means that a type is covariant to another type. Well, we could say:

  • A class is said to be covariant with respect to another class if it extends that other class;
  • A class is said to be covariant with respect to an interface if it implements that interface;
  • An interface is said to be covariant with respect to another interface if it implements that other interface.

class ParentClass {
    protected Object printMessage(String name) {
        System.out.println("Hi " + name);
        return name;
    }
}

class ChildClass extends ParentClass {
    /*
    * This is a valid overridden version: same name, same parameter 
    * list, more permissive access modifier (from protected to public)
    * and covariant return type (remember that all reference type have
    * Object as parent)
    */
    public String printMessage(String name) {
        System.out.println("Bye bye " + name);
        return name;
    }
}

What about the fifth rule? We will see that in more details next week, when we will discuss about the difference between overriding and hiding. I hope for now to have at least clarify a bit the main points behind method overloading and overriding. Stay tuned for the next episode!

by Ilenia Salvadori