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 ?
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
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
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
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With