javagenericstype-inferencemethod-referencejls

Conditions for Method Reference Expression to be "exact"


Consider the following article from the JLS (§15.13.1)

A method reference expression ending with Identifier is exact if it satisfies all of the following:

  • If the method reference expression has the form ReferenceType ::[TypeArguments] Identifier, then ReferenceType does not denote a raw type.
  • The type to search has exactly one member method with the name Identifier that is accessible to the class or interface in which the method reference expression appears.
  • This method is not variable arity (§8.4.1).
  • If this method is generic (§8.4.4), then the method reference expression provides TypeArguments.

Consider the following code snippet:

class Scratch {

  public static void main(String[] args) {
    Scratch.funct(new ImplementingClass()::<Functional1>hitIt);
  }

  public static void funct(Functional1 a){}
  public static void funct(Functional2 a){}
}
interface Functional1 {<T> T hitIt();}
interface Functional2 {<T> T hitIt();}

class ImplementingClass{
  public <T> T hitIt(){return null;}
}

Clearly - this satisfies all the conditions being mentioned for a method reference to be exact.

Not sure why still the method reference is in-exact in this particular case? Am I missing something here from the clause?

Solution :

Based on inputs from @Sweeper @DidierL and @Holger here what I summarized:

  1. Both the functional interfaces have the functionType <T> () -> T
  2. the method reference …::<Functional1>hitIt substitutes T with Functional1, so the resulting functional signature is () -> Functional1 which does not match <T> () -> T.

Solution

  • First a warning: IANAJL (IANAL for Java 😉)

    As far as I can tell, this should compile if you make the two interface methods non-generic, but it doesn’t. Let’s simplify the code as much as we can to reproduce the problem:

    class Scratch {
      public static void main(String[] args) {
        Scratch.funct(ImplementingClass::<Void>hitIt);
      }
    
      public static void funct(Functional1 a){}
      public static void funct(Functional2 a){}
    }
    interface Functional1 {Integer hitIt();}
    interface Functional2 {String hitIt();}
    
    class ImplementingClass{
      public static <T> Integer hitIt(){return null;}
    }
    

    The simplifications:

    Now let’s analyze the call to check if it should compile. I put links to the Java 8 specs but they are very similar in 17.

    15.12.2.1. Identify Potentially Applicable Methods

    A member method is potentially applicable to a method invocation if and only if all of the following are true:

    […]

    • If the member is a fixed arity method with arity n, the arity of the method invocation is equal to n, and for all i (1 ≤ in), the i'th argument of the method invocation is potentially compatible, as defined below, with the type of the i'th parameter of the method.

    […]

    An expression is potentially compatible with a target type according to the following rules:

    […]

    • A method reference expression (§15.13) is potentially compatible with a functional interface type if, where the type's function type arity is n, there exists at least one potentially applicable method for the method reference expression with arity n (§15.13.1), and one of the following is true:
      • The method reference expression has the form ReferenceType :: [TypeArguments] Identifier and at least one potentially applicable method is i) static and supports arity n, or ii) not static and supports arity n-1.
      • The method reference expression has some other form and at least one potentially applicable method is not static.

    (this last bullet applies for the case of the question where the method reference uses a constructor invocation expression, i.e. a Primary)

    At this point, we only check for the arity of the method reference, so both funct() methods are potentially applicable.

    15.12.2.2. Phase 1: Identify Matching Arity Methods Applicable by Strict Invocation

    An argument expression is considered pertinent to applicability for a potentially applicable method m unless it has one of the following forms:

    […]

    • An inexact method reference expression (§15.13.1).

    […]

    This is the only bullet point in this list that could potentially match, however, as pointed in the question we have an exact method reference expression here. Note that if you remove the <Void>, this makes it an inexact method reference, and both methods should be applicable as per the next section:

    Let m be a potentially applicable method (§15.12.2.1) with arity n and formal parameter types F1 ... Fn, and let e1, ..., en be the actual argument expressions of the method invocation. Then:

    […]

    • If m is not a generic method, then m is applicable by strict invocation if, for 1 ≤ in, either ei is compatible in a strict invocation context with Fi or ei is not pertinent to applicability.

    However only the first funct() method declaration should be applicable by strict invocation. Strict invocation contexts are defined here, but basically they check if the type of the expression matches the type of the argument. Here the type of our argument, the method reference, is defined by section 15.13.2. Type of a Method Reference whose relevant part is:

    A method reference expression is compatible in an assignment context, invocation context, or casting context with a target type T if T is a functional interface type (§9.8) and the expression is congruent with the function type of […] T.

    […]

    A method reference expression is congruent with a function type if both of the following are true:

    • The function type identifies a single compile-time declaration corresponding to the reference.

    • One of the following is true:

      • The result of the function type is void.
      • The result of the function type is R, and the result of applying capture conversion (§5.1.10) to the return type of the invocation type (§15.12.2.6) of the chosen compile-time declaration is R' (where R is the target type that may be used to infer R'), and neither R nor R' is void, and R' is compatible with R in an assignment context.

    Here R would be Integer for Functional1 and String for Functional2, while R' is Integer in both cases (since there is no capture conversion needed for ImplementingClass.hitIt()), so clearly the method reference is not congruent with Functional2 and by extension not compatible.

    funct(Functional2) should thus not be considered for applicability by strict invocation, and since only funct(Functional1) remains it should be selected.

    It should be noted that Javac must select both methods in Phase 1, because only one phase can apply, and Phase 2 only uses loose context instead of strict, which just allows boxing operations, and Phase 3 then includes varargs, which is not applicable either.

    Except if we consider that Javac somehow considers the method reference as congruent with Functional2, the only reason I see for selecting both methods is if it considered the method reference as not pertinent for applicability as specified above, which I can only explain if the compiler considers it as an inexact method reference.

    15.12.2.5. Choosing the Most Specific Method

    This is where the compilation fails. We should note that there is nothing here that would make the compiler select one method over the other. The applicable rule is:

    • m2 is not generic, and m1 and m2 are applicable by strict or loose invocation, and where m1 has formal parameter types S1, ..., Sn and m2 has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

    […] A type S is more specific than a type T for any expression if S <: T (§4.10).

    This appears to work properly: change Functional2 to extend Functional1 and it will compile.

    A functional interface type S is more specific than a functional interface type T for an expression e if T is not a subtype of S and one of the following is true (where U1 ... Uk and R1 are the parameter types and return type of the function type of the capture of S, and V1 ... Vk and R2 are the parameter types and return type of the function type of T):

    • If e is an explicitly typed lambda expression […]
    • If e is an exact method reference expression (§15.13.1), then i) for all i (1 ≤ i ≤ k), Ui is the same as Vi, and ii) one of the following is true:
      • R2 is void.
      • R1 <: R2.
      • […]

    This does not allow to disambiguate it either. However, changing Functional2.hitIt() to return Number should make Functional1 more specific since Integer <: Number.

    This still fails, which seems to confirm that the compiler does not consider it as an exact method reference.

    Note that removing the <T> in ImplementingClass.hitIt() allows it to compile, independently of the return type of Functional2.hitIt(). Fun fact: you can leave the <Void> at the call site, the compiler ignores it.

    Even stranger: if you leave the <T> and add more type arguments than required at the call site, the compiler still complains about the ambiguous call and not about the number of type arguments (until you remove the ambiguity). Not that this should make the method reference inexact, based on the above definition, but I would think it should be checked first.

    Conclusion

    Since the Eclipse compiler accepts it, I would tend to consider this as a Javac bug, but note that the Eclipse compiler is sometimes more lenient than Javac with respect to the specs, and some similar bugs have been reported and closed (JDK-8057895, JDK-8170842, …).