You've misunderstood.
ALL methods in java resolve to the one that it was constructed with.
Java uses a two-hit model to figure out what actual method to invoke:
- Bind to the right signature (static).
- Find the right overload to call (dynamic dispatch).
Bind to the right signature
Methods in java have a signature. The signature includes the name, the types of the parameters, and the return type (though that last one is usually irrelevant; javac won't compile any code with 2 methods that differ only in return type and nothing else). Note that generics are not taken into account here.
Example:
public void foo()
[1]
public void foo(String[] x)
[2]
public void foo(String... x)
[2]
public void foo(List<String> x)
[3]
public void foo(List<Integer> x)
[3]
public void foo(List<String> x, boolean whatever)
[4]
here each entry with the same number has the same signature: The [3]s are considered the same signature because generics are wiped out first, and the [2]s are considered the same signature because varargs is implemented as an array.
Java will use the type of the expression you are invoking the method on to figure out which signature you're attempting to invoke. This is entirely a compile time affair!
Example:
class Parent {
public void foo(Object arg) { System.out.println("Parent"); }
}
class Child extends Parent {
public void foo(String arg) { System.out.println("Child"); }
// note: not the same signature!
}
Parent p = new Child();
p.foo("Hello");
javac (the compiler) looks at the line p.foo("Hello")
, and will do the 'which signature is this?' dance: p is of type Parent
(the fact that it is pointing at an object of type Child is irrelevant; the compiler cannot know this and doesn't take it into account), looks at all methods that the type Parent
has, sees only one foo
, and thus, that call is using the signature: void foo(Object)
. This code will print Parent
if run.
Dynamic dispatch
Once the signature is figured out by the compiler, that is encoded in the class file. When that class file is executed, dynamic dispatch is used: The actual object's type is checked, and the most specific version is invoked. Example:
class Parent {
public void foo(Object arg) { System.out.println("Parent"); }
}
class Child extends Parent {
public void foo(Object arg) { System.out.println("Child"); }
// note: now it IS the same signature!
}
Parent p = new Child();
p.foo("Hello");
note how it is the same example, except now the signature of the child method matches the one of the parent - it is now 'the same method'. Thus, whilst the compilation of the p.foo("Hello")
call didn't change at all (in fact, you can recompile JUST Child.java and not recompile the code that invokes p.foo, and you'll observe this: Now it prints Child
. That part is entirely a runtime affair.
I strongly advise you to use the @Override
annotation anytime you intend to 'override' a method, so, the foo(Object)
in the Child.java
file should have that annotation. That annotation does only one thing: IF you put it on a method that isn't overriding anything (as in, there is no parent type that has the same method (signature and all!) as this), it's a compiler error, otherwise, it has no effect. Compiler-checked documentation is a good thing.
Had you put @Override
on public void foo(String arg)
, you'd have gotten an error. Whilst it has the same name as a method in parent, it's not the same signature.
TL;DR: In java, 'method names' include all the generics-free types of the params, so if the param lists don't match up, it's not the same method. But if they do, dynamic dispatch is always used, you can't opt out of this in java: The method invoked will be the one of the actual object you're invoking it on.
NB: If methods are static, there is no dynamic dispatch at all. It's all static (determined at compile time).