According to this https://docs.oracle.com/javase/specs/jls/se18/html/jls-15.html#jls-15.12.2.5, the Java compiler will attempt to choose the most specific method to invoke when multiple applicable and accessible ones are available. The intuition is that more specific method can be substituted by less specific ones but not vice versa.
So I'm a bit surprised that this won't work when we wrap the ambiguous call within a generic wrapper as follows:
public class Test{
static <T> void direct(T t) { System.out.println("generic");}
static void direct(int t) { System.out.println("specific-int");}
static <T> void indirect(T t) { direct(t);}
public static void main ( String [] args ) {
direct(1); // print specific-int
indirect(1); // print generic
}
}
So we can see that just calling direct
makes it works as expected. But when calling indirect
, the less specific method is called instead.
The behavior changes if I change the type of the direct method as such
static void direct(short t) { System.out.println("specific-short");}
In this case both lines print generic
instead (the short
method isn't called in either case), which tells me that the literal 1
was implicitly casted to int
in the first instance. If that's the case, why wasn't it called with the more specific method that takes in int
as an argument?
CodePudding user response:
2 unrelated reasons, both of which lead to the generic version:
Boxing
type parameters are neccessarily subtypes of Object (for now, Project Valhalla is a java update in the pipeline that may change this somewhat).
Hence, the direct(t)
call in your indirect
method cannot possibly treat direct(int)
as the most specific version of it, as the t
parameter in indirect
cannot possibly be an int
.
Specifically, when you invoke indirect(1)
, that 1 is an int
which isn't a valid value for indirect
(as indirect
's parameter is of type T
; T is a type parameter with as lower bound java.lang.Object
, and 1
isn't a valid value for Object
).
However, java also has the concept of 'boxing', where primitives will be converted to their boxed type automatically, but only if the code wouldn't have compiled otherwise. You can see this with javap
- you'll notice that the compiler replaced 1
with Integer.valueOf(1)
to make it work.
Names are compile-time
Which method is chosen is locked in at compile time. Note that overrides (where a subtype implements the exact same method, i.e. if that method is annotated with @Override
, the compiler will accept it) are entirely a runtime thing and java always picks the implementation from the subtype, but static
doesn't "do" subtyping, so it's not relevant here.
The 2 direct
methods you have are not the same; at the JVM level, they have completely different names, so this is just about which of the 2 direct
methods is picked by javac
, hence, compile time is the only time that matters here.
Let me make that clear: The 2 direct
methods do not have the same name, therefore at runtime the JVM does not have the freedom to pick one or the other depending on types. For the same reason it can't replace a call to foo()
with a call to bar()
- not the same name.
indirect
, the method, has no idea what T is. Therefore it could not possibly call direct(int)
even if the boxing thing wasn't the case! - looking up which of the two direct
methods to pick is not done at runtime.
To reiterate
Given:
public class Parent {
void foo(int i) { System.out.println("Parent-int"); }
void foo(Integer i) { System.out.println("Parent-Integer"); }
}
class Child extends Parent {
void foo(int i) { System.out.println("Child-int"); }
}
...
Parent p = new Child();
p.foo(5); // prints Child-int
p.foo(Integer.valueOf(5)); // prints Parent-Integer
a final note
You wrote in a comment:
"which tells me that the literal 1 was implicitly casted to int in the first instance"
No. integer literals in java are int
- the java lang spec defines them as such. You can write short x = 5;
only because of a special exemption rule that states you don't need the cast there, but languagewise, all non-decimal-pointed (and non 0x0p
form) numeric AST nodes are treated as int
. They are THEN implicitly casted if e.g. a long
is needed.
You can ask javac to treat things as long by sticking a capital L at the end. Given:
void foo(byte i) {System.out.println("byte");
void foo(short i) {System.out.println("short");
void foo(int i) {System.out.println("int");
void foo(long i) {System.out.println("long");
foo(5); // prints 'int'
foo(5L); // prints 'long'
Even though '5' fits in 'byte', it is an int, and hence the int
variant is chosen.
CodePudding user response:
why wasn't it called with the more specific method that takes in int as an argument?
The Java compiler decides which method to invoke at compile time. That is, for the indirect
method, it chooses an overload of direct
which can be safely invoked for all invocations of indirect
.
The only such overload is the direct(T)
method: this accepts any Object
parameter, as does indirect(T)
. It can't invoke direct(int)
because not all Object
s are Integer
s.
indirect
doesn't do anything different when invoked with an int
parameter.