Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class type of reference and the actual class type, which decides which method to call? [duplicate]

The title may be misleading but as a non-native i couldn't figure out a better one.

Say i have two classes, Dog and Fox :

public class Dog {
    public String bark() {
        return "Wuff";
    }
    public String play(Dog d) {
        return "Wuff" + d.bark();
    }
}


public class Fox extends Dog {
    public String bark() {
        return "Ringding" ;
    }
    public String play(Fox f) {
        return "Ringding" + f.bark();
    }
}

And i create some instances as well call some methods

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(hybrid.play(foxi)); // Output number 1
System.out.println(foxi.play(hybrid)); // Output number 2

For the 1. Ouput I expected "RingdingRingding" because hybrid actually is a reference to an instance of the Dog, even if the reference has a type of Dog it still refers to an Fox object, but still i got this output :

WuffRingding

The second one i got the same problem, since foxi is an instance of Fox and hybrid is actually an instance of Fox (no matter what reference, right ?), the ouput should be "RingdingRingding" but then again, i got :

WuffRingding

Can someone explain why ?

like image 344
Anh Tuấn Phạm Avatar asked Feb 14 '17 12:02

Anh Tuấn Phạm


3 Answers

Two important things for method invocations.

You have two times : the compilation time and the runtime.
And rules are not the same between these two times.

  • at compile time the compiler has to definite statically which exact signature of method is called to compile fine.
    This binding is static as the compiler doesn't matter of the concrete instance on which the method is invoked and it is the same thing for the parameters passed to the method.
    The compiler doesn't rely on the effective types as at runtime the effective types could change during the execution flow.
    So the compiler searches among available methods for a declared type, which is the more specific method according to the declared types of parameters passed to it.

  • at runtime an instance method from a class or from another one will be used according to the effective instance on which the method is invoked.
    But the invoked method has to respect the signature specified at compile time.

1)For the first case :

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(hybrid.play(foxi)); // Output number 1
  • First time (compîle time) :

For a Dog instance, the compiler has to find the most specific method play() with as parameter a variable with the Fox declared type.

In Dog class, a single method play() with a compatible signature exist :

public String play(Dog d) {

So this signature is used for the binding : String play(Dog d).

About the bark() method, it is much obvious as there is only one bark() method signature .
So we have no ambiguity on the method that is bound at compile time

  • Second time (runtime) :

At runtime the String play(Dog d) method of the concrete instance is invoked. The hybrid variable refers to an instance of Fox but Fox doesn't override String play(Dog d). Fox defines a play() method but with another signature :

public String play(Fox f) {

So the JVM invokes the public String play(Dog d) { method of Dog.
And then it invokes the method of the effective type of d when d.bark() is executed and d refers to a Fox instance.

So "WuffRingding" is output.


2) For the second case :

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(foxi.play(hybrid)); // Output number 2
  • First time (compile time) :

For a Fox instance, the compiler has to find the most specific method play() with as parameter a variable with the Dog declared type.

In Fox class, two play() methods with a compatible parameter exist :

public String play(Dog d) { // inherited from the parent class

public String play(Fox f) { // declared in Fox

The compiler has to choose the more specific method for the call context of the method
It identifies a method more specific that the other one for a Dog declared type parameter : public String play(Dog d). So the compiler binds the play() method invocation to public String play(Dog d) when it compiles the class.

  • Second time (runtime) :

At runtime the String play(Dog d) method of the concrete instance is invoked.
As for the first case, the foxi variable refers to an instance of Fox but Fox doesn't override String play(Dog d).
So the JVM invokes the public String play(Dog d) method of Dog.
And then it invokes the method of the effective type of f when f.bark() is executed and f refers to a Fox instance.

So "WuffRingding" is again output.


To avoid this kind of surprise you should add @Override in the methods designed to override a parent class method :
For example :

@Override
public String play(Fox f) {
    return "Ringding" + f.bark();
}

If the method doesn't override effectively a play(Fox f) method in the hierarchy, the compiler will complain about it.

like image 148
davidxxx Avatar answered Oct 09 '22 16:10

davidxxx


Apparently, the thing which causes your confusion is that you think the play-method in the subclass Fox overrides the play-method of the superclass, while it actually just overloads it.

If you change the parameter type of f in the play-method of the Fox-class to type Dog, the output will be "RingdingRingding" both times for the reasons you analyzed in your question, because in this case the play-method correctly overrides the superclass-method.

Let's look into the case with the overloaded play-methods in more detail:

hybrid.play(foxi)

The compiler looks at the declared static type of hybrid which is Dog. foxi is declared as Fox. Therefore, the compiler looks for a play-method in class Dog which expects one parameter of static type Fox. It is not successful, because there is only a play-method which expects one parameter of static type Dog. However, the compiler will still pick this method for invocation in the end, because Dog is a superclass of Fox.

foxi.play(hybrid)

The compiler looks at the declared static type of foxi which is Fox. hybrid is declared as Dog. Therefore, the compiler looks for a play-method in class Fox which expects one parameter of static type Dog. There are two play-methods in class Fox the compiler can choose from: play(Fox f) and the inherited overloaded method play(Dog d). play(Fox f) is not a valid choice since this method expects one parameter of type Fox, while hybrid is declared as Dog. This means the compiler will again pick the play(Dog d) method, which is declared in class Dog, like for the previous statement.

The reason why the compiler doesn't allow you to override play(Fox f) with play(Dog d) is the following: Imagine it was allowed and someone would do this:

Dog doggo = new Dog();
hybrid.play(doggo);

Now, the overriden play(Fox f) method would be called with an input-parameter of type Dog at runtime which doesn't work, because the implemenatation of play(Fox f) expects that the f is not only a Dog, but a more specialized Fox.

To avoid overload/override confusion annotate methods which should override superclass-methods with @Override. The compiler will refuse to compile your code if a with @Override annotated method actually does not override a superclass method.

like image 42
Calculator Avatar answered Oct 09 '22 18:10

Calculator


Here in your case The play method is overloaded not overridden.

When you do this Dog d = new Fox() The reference of Dog will call the method of class Dog only until and unless the methods of Dog class are overridden by Fox class. When the methods are overridden invoking of such methods is resolved at runtime but the invoking of overloaded methods is resolved at compile time.

Read static polymorphism and run-time polymorphism for further clearance.

like image 1
BeginnersSake Avatar answered Oct 09 '22 17:10

BeginnersSake