McGlone
McGlone

Reputation: 3444

Selection of the Correct Method for Invocation

Take the following code:

public class Parent
{
    public String doIt(Object o) {
        return "parent";
    }
}

public class Child extends Parent
{
    public String doIt(Object s) {
        return super.doIt(s) + ": " + "child";
    }
}

public class Poly
{
    public String makeItHappen() {
        Parent p = new Child();
        return p.doIt("test");
    }
}

Due to the fact that Child.doIt() overrides Parent.doIt(), invoking Poly.makeItHappen() results in this being printed to the console:

parent: child

However, if I make a change in Child.doIt() to look like this:

public String doIt(String s) {
    return super.doIt(s) + ": " + "child";
}

Now Child.doIt() does not override Parent.doIt(). When you invoke Poly.makeItHappen(), you get this result:

parent

I'm a bit puzzled by this. The compile-time type of p is Parent so I certainly understand it finding Parent.doIt() as a potentially applicable method, but, given that the run-time type of p is Child, I'm not certain why Child.doIt() is not. Presuming that they are both identified as potentially applicable methods, I would expect Child.doIt(String) to be invoked over Parent.doIt(Object) as it is more specific.

I've tried consulting the JLS and found this bit:

15.12.1 Compile-Time Step 1: Determine Class or Interface to Search

...

In all other cases, the qualified name has the form FieldName . Identifier; then the name of the method is the Identifier and the class or interface to search is the declared type T of the field named by the FieldName, if T is a class or interface type, or the upper bound of T if T is a type variable.

To me, that says that the compile-time type of p would be used. This makes some sense when I see Parent.doIt(Object) being invoked while Child.doIt(String) is not. But it doesn't make sense in the terms of the polymorphic behavior noted when Child.doIt() properly overrides Parent.doIt() - in that case, both methods of Parent and Child are being analyzed to find potentially applicable methods so why not in the second case, as well?

I know I'm missing something here, but I just can't figure out why I'm seeing the behavior that I am. If anyone could shed some insight into this, I'd appreciate it.

EDIT: FOUND THE ANSWER:

Thanks to Jack's response, I was able to find the answer within the JLS. The section of the JLS I referred to earlier was actually focused on finding the correct method at compile time and did not cover the process of invoking the correct method at run time. That portion of the process can be found the section of the JLS titled 15.12.4 Runtime Evaluation of Method Invocation.

In there, I found this bit of text:

Otherwise, the invocation mode is interface, virtual, or super, and overriding may occur. A dynamic method lookup is used. The dynamic lookup process starts from a class S, determined as follows:

My invocation mode is virtual, so the above statement applies...

If the invocation mode is interface or virtual, then S is initially the actual run-time class R of the target object.

...

The dynamic method lookup uses the following procedure to search class S, and then the superclasses of class S, as necessary, for method m.

Okay, so this seemed very odd to me. According to this, the JVM is going to start looking for an applicable method in Child to invoke, which would lead me to believe that Child.doIt(String) would be invoked. But, reading on...

Let X be the compile-time type of the target reference of the method invocation.

...

If class S contains a declaration for a non-abstract method named m with the same descriptor (same number of parameters, the same parameter types, and the same return type) required by the method invocation as determined at compile time (§15.12.3), then:

The class "S", which would be Child, does indeed contain a method with the same descriptor as the method invocation determined at compile time (String "is a" Object, after all, so the descriptors are identical). It still seems to be like Child.doIt(String) should be getting invoked, but reading on...

If the invocation mode is virtual, and the declaration in S overrides (§8.4.8.1) X.m, then the method declared in S is the method to be invoked, and the procedure terminates.

...

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

The bit in bold is the really important part of this. As I mentioned, when I changed the method Child.doIt(), it no longer was overriding the doIt() method from Parent. So, even though the JVM is evaluating the Child.doIt() method as a potential candidate for invocation, it fails to be invoked because it does not override the method defined in X, which is Parent. I was really getting hung up because I thought the JVM was not even check Child.doIt as a potentially applicable method, and that didn't seem correct. Now I believe that the JVM is checking that method as a potentially applicable method, but then disregards it because it doesn't properly override the parent method. It's a situation in which I thought that the method from the subclass would be invoked, not because it overrides the parent class method, but because it is the most specific. In this case, however, that isn't true.

The next line in the JLS simply explains that this procedure executes recursively over superclasses, leading Parent.doIt(Object) to be invoked.

Intuitively, this made complete sense to me but I just couldn't wrap my head around how the JVM was actually executing this process. Of course, looking in the correct part of the JLS would have helped a great deal.

Upvotes: 1

Views: 287

Answers (5)

Affe
Affe

Reputation: 47954

Hmm, trying to address the 'why' the runtime doesn't play hide and go seek with the entire class definition looking for 'better' matches to a method than the one the compiler asked for, will go for an example. Obviously this API is horrible in the first place, but you can imagine how the situation could happen accidentally with very long method signatures.

/**
* Vendor API you program to
*/
public class IPv4Manager {
  public void terminateSocket(Object obj) {
    //terminate IPv4 socket
  }
}

/**
* Vendor class that's injected at runtime that you have no knowledge of 
* and do not compile against.
*/
public class IPv4And6Manager extends IPv4Manager {

  public void terminateSocket(Byte[] packet) {
    //terminate IPv6 socket
  }

}

/**
*Your user code
*/
public void terminateIPv4Socket(Byte[] packet) {
  IPv4Manager manager = managerFactory.getV4Manager(); //returns you an instance of 4And6Manager
  manager.terminateSocket(packet);
}

Do you really want the runtime to try to outsmart you and call a "better" method than the one the compiler asked for?

Upvotes: 0

Brian Roach
Brian Roach

Reputation: 76898

You upcasted the Child to Parent

Parent p = new Child();

When you invoke p.doIt("test"); the only way you're going to get Child behavior is via an overridden method.

Since you changed the method in Child to doIt(String s), it is no longer overriding anything in Parent and so doIt(Object o) is called in the Parent.

Upvotes: 0

ccoakley
ccoakley

Reputation: 1

The method signature to be invoked is determined at compile time, so p.doIt("test") will invoke the doIt(Object o) method of the appropriate class at runtime. doIt(String s) isn't even looked at. Imagine that Child.java didn't exist at the time that the Poly makeItHappen was written--you would also have to abstract the Child constructor away into a factory method in a different class to get it to compile. You could re-implement the factory method and Child.java without ever recompiling Poly.java. This allows you to program to the interface provided by the base class and relatively efficient function invocation. Your Poly makeItHappen only needs to know that a doIt(Object o) exists.

If you think of the possible implementation of inheritance as through a virtual function table, then doIt(String s) and doIt(Object o) have different table entries. Parent only has an entry for doIt(Object o). Child has both an entry for doIt(Object o) that is the same body as Parent and an entry for doIt(String s) that is its own. At compile time, the method to be invoked is the one in the doIt(Object o) slot.

Upvotes: 0

Jack
Jack

Reputation: 133567

Dynamic binding of the method applies at runtime between different implementations with the same signature but the set of all possible methods is determined statically when the compilers look at the signature. Since you declare p as a Parent class, at runtime it will look for a method that is present in the Parent class and, if a more specific implementation is present (because of a subclass, like in your example), then it will be chosen instead that the ancestor one.

Since your method does not override anything, at compile time it will choose a different signature (the one with Object) and ignore it. It doesn't appear as a matching possibility at runtime because of the type of attribute p.

Upvotes: 3

Zach
Zach

Reputation: 897

Your answer is actually pretty simple: your change to the method broke your use of inheritance.

The Parent only has one doIt method, and it take an Object parameter. When you call doIt("test") on the parent, it looks to see if it is overridden in the child. Since doIt(Object s) is not overridden in the child, the parent method is utilized. Even though, you pass a String, the parent method will still be called since doIt(Object s) is not the same as doIt(String s).

Simply put, you are not overriding the method when you change the signature, you are overloading it.

Upvotes: 0

Related Questions