I was testing a code when I faced something strange that I could not figure out why this is happening. So I will give a simple example of what happened there.
Consider these classes
public class A {
public void print(A a) {
System.out.println("A");
}
}
public class B extends A {
public void print() {
System.out.println("B");
}
}
public class C extends B {
public void print(A a) {
System.out.println("C");
}
}
public class E extends C {
public void print(E e) {
System.out.println("E");
}
}
Now in my main method I have this kind of instantiation:
B b = new E();
C c = new E();
E e = new E();
I call these methods and get the following output.
b.print(b);
c.print(e);
OUTPUT:
C
C
I have two explanations but each of them is contradictory to one of these method calls. Also it is possible that both of my explanations are totally wrong so I am not claiming anything here.
Explanation 1
b.print(b)
: b
is an instance of E
and is upcasted to B
. B
have a print
method that takes an argument of type A
but it is overloaded by class E
. However the argument to this method is b
which is upcasted to B
, and therefore the method call matches the signature of C.print(A a)
(since C
is a superclass of E
) and therefore the result is rational.
c.print(e)
: Thinking the same way as above is not going to explain the output here. c
is an instance of E
and is upcasted to C
. C
have a print
method that takes an argument of type A
but it is overloaded by class E
. But in contrast to the above case, the argument to this method is e
which matches the signature of E.print(E e)
. So By this reasoning the output should have been E which is not!
Explanation 2
here I start with the second method call and reason about that.
c.print(e)
: c
is an instance of E
and is upcasted to C
. C
have a print
method that takes an argument of type A
. The argument to this method is e
which is an instance of E
which in turn is a subclass of A
. Since it has been upcasted E.print(E e)
is hidden from c
. Therefore, the method call matches the signature of C.print(A a)
and the output is rational by this logic.
b.print(b)
: b
is an instance of E
and is upcasted to B
. B
have a print
method that takes an argument of type A
(again it is hidden). Hence, the method call matches the signature of A.print(A a)
and by this logic the output should have been A which is not.
I am really really confused about what is going on here. Could someone please explain.
Which method will be called is decided in two steps:
At runtime it is decided which of the overridden methods will be used based on the actual instance type (polymorphism)
b.print(b); // B b = new E();
At compile time, since the declared type of b
is B
, print
accepting an instance of B
(or its superclasses (A
)) can only be used, which means: A.print(A a)
At runtime, once the overloaded methods has been selected in the previous step, the actual type of b
(E
) is used to select the version of print(A a)
that will be used: C.print(A a)
over A.print(A a)
c.print(e); // C c = new E(); E e = new E();
At compile time, the declared type of c
is C
and the declared type of e
is E
so only these methods can be used: A.print(A a)
and C.print(A a)
At runtime, the actual type of e
is E
therefore the more specific (meaning higher in the class hierarchy) version is selected: C.print(A a)
I don't mean to pile on to tieTYT's answer, but I thought it might be instructive to look at the bytecode for your main
method. I wrapped your code in an outer class called Foo
and used javap -classpath . -c -s Foo
to disassemble it. Here's what we get:
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
Code:
0: new #2 // class Foo$E
3: dup
4: invokespecial #3 // Method Foo$E."<init>":()V
7: astore_1
8: new #2 // class Foo$E
11: dup
12: invokespecial #3 // Method Foo$E."<init>":()V
15: astore_2
16: new #2 // class Foo$E
19: dup
20: invokespecial #3 // Method Foo$E."<init>":()V
23: astore_3
24: aload_1
25: aload_1
26: invokevirtual #4 // Method Foo$B.print:(LFoo$A;)V
29: aload_2
30: aload_3
31: invokevirtual #5 // Method Foo$C.print:(LFoo$A;)V
34: return
The interesting lines are 26 and 31. Notice that on both lines the compiler selected methods that take arguments of type A
, but that seems to violate our intuition. We expected E.print(E)
on line 31, but we didn't get it.
This happens because the Java compiler doesn't know the actual types of the b
, c
, and e
variables at compile time; it only knows their declared types. In this case you're using a class constructor to create the objects, but imagine if you used static factory methods instead. The types of those variables could vary based on very complex logic, possibly involving reflection. The compiler may be able to determine their actual types in some cases, but it can't possibly do it in all cases. As a result, the compiler has to decide which method to call based on the declared type of the variables rather than their actual type.
You might be wondering why line 26 prints "C" even though the bytecode says to call B.print(A)
... and wait a minute, B
doesn't even declare a print(A)
method; it inherits print(A)
from A
. So why doesn't the bytecode for line 26 say // Method Foo$A.print:(LFoo$A;)V
?
That's where method overriding comes in. At runtime, the Java interpreter will use the actual type of the object to determine which version of print(A)
gets called. Since both objects are of type E
, and E
doesn't have a print(A)
method of its own, Java winds up calling C.print(A)
.
I've modified your sample code to look like this:
package com.sandbox;
public class Sandbox {
public static void main(String[] args) {
B b = new E();
C c = new E();
E e = new E();
b.print(b); //C
c.print(e); //C
e.print(e); //E
e.print(b); //C
}
public static class A {
public void print(A a) {
System.out.println("A");
}
}
public static class B extends A {
public void print() { //doesn't override or overload anyone
System.out.println("B");
}
}
public static class C extends B {
public void print(A a) { //overrides "A"
System.out.println("C");
}
}
public static class E extends C {
public void print(E e) { //Overloads A's print
System.out.println("E");
}
}
}
Since E's method is just overloaded, you can rename it like this:
package com.sandbox;
public class Sandbox {
public static void main(String[] args) {
B b = new E();
C c = new E();
E e = new E();
b.print(b); //C
c.print(e); //C
e.unrelatedMethod(e); //E
e.print(b); //C
}
public static class A {
public void print(A a) {
System.out.println("A");
}
}
public static class B extends A {
public void print() { //doesn't override or overload anyone
System.out.println("B");
}
}
public static class C extends B {
public void print(A a) { //overrides "A"
System.out.println("C");
}
}
public static class E extends C {
public void unrelatedMethod(E e) {
System.out.println("E");
}
}
}
And things start to make more sense. I think what's really confusing about your sample is that your methods have the same name, but they aren't really the same in the same way.
Let me know if this makes it clear. Those two samples are exactly the same, the only difference is the naming is more clear.
This is the relevant documentation, but the short story is:
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