Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does polymorphism in Java work for this general case (method with parameter)?

I have the code for a general case:

public class A {
    public String show(A obj) {
        return ("A and A");
    }
}

public class B extends A {
    public String show(B obj) {
        return ("B and B");
    }

    public String show(A obj) {
        return ("B and A");
    }
}

public class C extends B {

}

public class Test {
    public static void main(String[] args) {
        A a = new B();
        B b = new B();
        C c = new C();

        System.out.println("1--" + a.show(b));
        System.out.println("2--" + a.show(c));     
    }
}

The results are:

1--B and A
2--B and A

I know there is a priority chain from high to low in Java:

this.show(O), super.show(O), this.show((super)O), super.show((super)O)

My understanding is below:

In this code:

A a = new B()

An upcast happens. A is a parent class reference and B is a child parent class reference. When the code is compiled and run, the child parent class reference determines how the method is chosen. In this case, show(A) in class B is chosen.

There is also a requirement that polymorphism has to meet: The method that is chosen should be included in the parent class definition.

Could someone give a more detailed explanation on the result shown?

like image 317
CandyCrusher Avatar asked Oct 12 '18 09:10

CandyCrusher


2 Answers

In order to get why you get the result B and A twice, you need to know that there are 2 parts to this: compilation and runtime.

Compilation

When encountering the statement a.show(b), the compiler takes these basic steps:

  1. Look at the object that the method is called on (a) and get its declared type. This type is A.
  2. In class A and all of its super types, make a list of all methods that are named show. The compiler will find only show(A). It does not look at any methods in B or C.
  3. From the list of found methods, choose the one that best matches the parameter (b) if any. show(A) will accept b, so this method is chosen.

The same thing will happen for the second call where you pass c. The first two steps are the same, and the third step will again find show(A) since there is only one, and it also matches the parameter c. So, for both of your calls, the rest of the process is the same.

Once the compiler has figured out what method it needs, it will create a byte-code instruction invokevirtual, and put the resolved method show(A) as the one it should call (as shown in Eclipse by opening the .class):

invokevirtual org.example.A.show(org.example.A) : java.lang.String [35]

Runtime

The runtime, when it eventually gets to the invokevirtual needs to do a few steps also.

  1. Get the object on which the method is called (which is already on the stack by then), which is a.
  2. Look at the actual runtime type of this object. Since a = new B(), this type is B.
  3. Look in B and try to find the method show(A). This method is found since B overrides it. If this had not been the case, it would look in the super classes (A and Object) until such a method is found. It is important to note that it only considers show(A) methods, so eg. show(B) from B is never considered.
  4. The runtime will now call method show(A) from B, giving the String B and A as result.

More detail about this is given in the spec for invokevirtual:

If the resolved method is not signature polymorphic (§2.9), then the invokevirtual instruction proceeds as follows.

Let C be the class of objectref. The actual method to be invoked is selected by the following lookup procedure:

If C contains a declaration for an instance method m that overrides (§5.4.5) the resolved method, then m is the method to be invoked, and the lookup procedure terminates.

Otherwise, if C has a superclass, this same lookup procedure is performed recursively using the direct superclass of C; the method to be invoked is the result of the recursive invocation of this lookup procedure.

Otherwise, an AbstractMethodError is raised.

For your example, the objectref is a, its class is B and the resolved method is the one from the invokevirtual (show(A) from A)


tl:dr - Compile-time determines what method to call, runtime determines where to call it from.

like image 67
TiiJ7 Avatar answered Oct 31 '22 19:10

TiiJ7


In your example A a = new B(), a is a polymorphic reference - a reference that can point different object from the class hierarchy (in this case it is a reference to object of type B but could also be used as a reference to object of class A, which is topmost in the object hierarchy).

As for specific behaviour you are asking about:

Why is B printed in the output?

Which specific show(B obj) method will be invoked through the reference variable depends on the reference to the object it holds at a certain point in time. That is: if it holds reference to object of class B, a method from that class will be called (that is your case) but if it would point to an object of class A, a reference of that object would be called. That explains why B is printed in the output.

hierarchy).

Why is and A printed in the output?

Method in a subclass with the same name but different signature is called method overloading. It uses static binding, which means that the appropriate method will be bound at compile-time. The compiler has no clue about the runtime type of your objects.

So show(A obj) of class A will be bound in that case. However, when the method will be actually called in runtime, its implementation from class B will be invoked (show(A obj) from class B) and that is why you see B and A and not A and A in the output.


Reference for invokevirutal (an JVM instruction called when virtual methods are executed):

If the resolved method is not signature polymorphic (§2.9), then the invokevirtual instruction proceeds as follows.

Let C be the class of objectref. The actual method to be invoked is selected by the following lookup procedure:

If C contains a declaration for an instance method m that overrides (§5.4.5) the resolved method, then m is the method to be invoked, and the lookup procedure terminates.

Otherwise, if C has a superclass, this same lookup procedure is performed recursively using the direct superclass of C; the method to be invoked is the result of the recursive invocation of this lookup procedure.

Otherwise, an AbstractMethodError is raised.


For the a.show(c), the same rules apply as for B because C doesn't have any methods overloaded and it is directly inheriting from B.

EDIT:

Step by step explanation of why a.show(c) prints B and A:

  1. Compiler recognizes object a to be objectref to object of class A (compile-time)
  2. Because a is of type A, method A::show(A obj) is bound.
  3. When the code is actually executed (i.e. show() method is invoked on object a), runtime recognizes that reference a polymorphically points to object of type B (that's because of A a = new B()) (runtime)
  4. Because C extends B, runtime treats a.show(c) as it would treat b.show(c) (or b.show(b)), so B::show(A obj) is used in this case but in place of obj an object of type B is used. That's why "B and A" is being printed.
like image 31
syntagma Avatar answered Oct 31 '22 19:10

syntagma