Search code examples
javaoopinheritanceoverridingoverloading

Invoking a Method of the Superclass expecting a Superclass argument on a Subclass instance by passing the an instance the of the Subclass


Can anyone please explain the output of the following code, and what's the Java principle involved here?

class Mammal {
    void eat(Mammal m) {
        System.out.println("Mammal eats food");
    }
}

class Cattle extends Mammal{
    void eat(Cattle c){
        System.out.println("Cattle eats hay");
    }
}

class Horse extends Cattle {
    void eat(Horse h) {
        System.out.println("Horse eats hay");
    }
}

public class Test {
    public static void main(String[] args) {
        Mammal h = new Horse();
        Cattle c = new Horse();
        c.eat(h);
    }
}

It produces the following output:

Mammal eats food

I want to know how we are coming at the above result.


Solution

  • Overloading vs Overriding

    That's not a valid method overriding, because all the method signatures (method name + parameters) are different:

    void eat(Mammal m)
    
    void eat(Cattle c)
    
    void eat(Horse h)
    

    That is called method overloading (see) and class Horse will have 3 distinct methods, not one. I.e. its own overloaded version of eat() and 2 inherited versions.

    The compiler will map the method call c.eat(h) to the most specific method, which is eat(Mammal m), because the variable h is of type Mammal.

    In order to invoke the method with a signature eat(Horse h) you need to coerce h into the type Horse. Note, that such conversion would be considered a so-called narrowing conversion, and it will never happen automatically because there's no guarantee that such type cast will succeed, so the compiler will not do it for you.

    Comment out the method void eat(Mammal m) and you will see the compilation error - compilers don't perform narrowing conversions, it can only help you with widening conversions because they are guaranteed to succeed and therefore safe.

    That what would happen if you'll make type casting manually:

    Coercing h into the type Horse:

    c.eat((Horse) h);
    

    Output:

    Cattle eats hay   // because `c` is of type `Cattle` method `eat(Cattle c)` gets invoked
    

    Because variable c is of type Cattle it's only aware of the method eat(Cattle c) and not eat(Horse h). And behind the scenes, the compiler will widen the h to the type Cattle.


    Coercing both c and h into the type Horse:

    ((Horse) c).eat((Horse) h);
    

    Output:

    Horse eats hay   // now `eat(Horse h)` is the most specific method
    

    Rules of Overriding

    The rules of method overriding conform to the Liskov substitution principle.

    Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

    The child class should declare its behavior in such a way so that it can be used everywhere where its parent is expected:

    • Method signatures must match exactly. I.e. method names should be the same as well as the types of parameters. And parameters need to be declared in the same order. It is important to note that if method signatures differ (for instance like in the code snippet provided in the question, name of one of the methods was misspelled) the compiler will have no clue that these methods are connected anyhow. I.e. it no longer be considered a case of overriding, methods will be considered to be distinct, and all other requirements listed below will not be applicable. That's why it's highly advisable to add the @Override annotation to the overridden method. With it, the compiler will give a clear feedback when it fails to find a matching method in the parent classes and interfaces, if you've misspelled the name, or declared parameters in the wrong order. Your IDE will add this annotation for you if you ask it to generate a template (shortcut in IntelliJ CTRL + O).

    • The access modifier of an overridden method can be the same or wider, but it can not be more strict. I.e. protected method in the parent class can be overridden as public or can remain protected, we can not make it private.

    • Return type of an overridden method should be precisely the same in case primitive type. But if a parent method declares to return a reference type, its subtype can be returned. I.e. if parent returns Number an overridden method can provide Integer as a return type.

    • If parent method declares to throw any checked exceptions then the overridden method is allowed to declare the same exceptions or their subtypes, or can be implemented as safe (i.e. not throwing exceptions at all). It's not allowed to make the overridden method less safe than the method declared by the parent, i.e. to throw checked exceptions not declared by the parent method. Note, that there are no restrictions regarding runtime exceptions (unchecked), overridden methods are free to declare them even if they are not specified by the parent method.

    This would be a valid example of method overriding:

    static class Mammal{
        void eat(Mammal m){
            System.out.println("Mammal eats food");
        }
    }
    
    public class Cattle extends Mammal{
        @Override
        void eat(Mammal c) {
            System.out.println("Cattle eats hay");
        }
    }
    
    public class Horse extends Cattle{
        @Override
        public void eat(Mammal h) throws RuntimeException {
            System.out.println("Horse eats hay");
        }
    }
    

    main()

    public static void main(String[] args) {
        Mammal h = new Horse();
        Cattle c = new Horse();
        c.eat(h);
    }
    

    Output:

    Horse eats hay