简体   繁体   中英

Java not choosing the most specific method when called through a generic wrapper?

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?

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, ie 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 eg 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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM