To put it simply:
- The compiler picks the signature of the method that will be invoked
- The runtime picks which implementation of the compiler-selected signature will run
In step 1, the compiler looks at the declared type of the object on which the method is invoked, and also at the parameter types, which means:
- the compiler only knows that
a
is A
("declared type", aka static type); it therefore can only search methods in A
or methods inherited by A
. This means that the compiler doesn't even search for doSomething
in class B
in your example.
- it looks at the data types of arguments to resolve one method among overloads. In your example, this is not necessary as there is only one
doSomething
method (the one with a float
parameter)
Based on the above, the compiler can only conclude that it's doSomething(float)
that will run. You can look at it as if the compiler's selected method signature is written in stone, the runtime can't change that.
In step 2, the runtime knows which signature will run (doSomething(float)
), the only thing it needs to do is pick which implementation of that signature will be invoked. For that, it looks at the actual object type (the object that was created), new B()
, in your example. It will then run the implementation in B
, if overridden, or search up the tree any overridden implementation of that exact signature until it gets to the inherited implementation in A
. Because B
does not override doSomething(float)
, the inherited implementation A.doSomething(float)
from A
runs.