简体   繁体   中英

How does Java know which overloaded method to call with lambda expressions? (Supplier, Consumer, Callable, ...)

First off, I have no idea how to decently phrase the question, so this is up for suggestions.

Lets say we have following overloaded methods:

void execute(Callable<Void> callable) {
    try {
        callable.call();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

<T> T execute(Supplier<T> supplier) {
    return supplier.get();
}

void execute(Runnable runnable) {
    runnable.run();
}

Going off from this table, I got from another SO question

Supplier       ()    -> x
Consumer       x     -> ()
BiConsumer     x, y  -> ()
Callable       ()    -> x throws ex
Runnable       ()    -> ()
Function       x     -> y
BiFunction     x,y   -> z
Predicate      x     -> boolean
UnaryOperator  x1    -> x2
BinaryOperator x1,x2 -> x3

These are the results I get locally:

// Runnable -> expected as this is a plain void  
execute(() -> System.out.println()); 

// Callable -> why is it not a Supplier? It does not throw any exceptions..
execute(() -> null);

// Supplier -> this returns an Object, but how is that different from returning null?
execute(() -> new Object());

// Callable -> because it can throw an exception, right?
execute(() -> {throw new Exception();});

How does the compiler know which method to call? How does it for example make the distinction between what's a Callable and what's a Runnable ?

I believe I have found where this is described in official documentation, although a bit hard to read.

Here is mentioned:

15.27.3. Type of a Lambda Expression

Note that while boxing is not allowed in a strict invocation context, boxing of lambda result expressions is always allowed - that is, the result expression appears in an assignment context, regardless of the context enclosing the lambda expression. However, if an explicitly typed lambda expression is an argument to an overloaded method, a method signature that avoids boxing or unboxing the lambda result is preferred by the most specific check (§15.12.2.5).

and then here (15.12.2.5) is described analytically how the most specific method is chosen.

So according to this for example as described

One applicable method m1 is more specific than another applicable method m2, for an invocation with argument expressions e1, ..., ek, if any of the following are true:

m2 is generic, and m1 is inferred to be more specific than m2 for argument expressions e1, ..., ek

So

// Callable -> why is it not a Supplier?
execute(() -> null);   <-- Callable shall be picked from 2 options as M2 is generic and M1 is inferred to be more specific

void execute(Callable<Void> callable) {  // <------ M1 
   try {
    callable.call();
  } catch (Exception e) {
      e.printStackTrace();
  }
}


 <T> T execute(Supplier<T> supplier) {  // <------ M2 is Generic
    return supplier.get();
 }

Why M1 is inferred to be more specific can be traced down from this process described here (18.5.4 More Specific Method Inference)

// Callable -> why is it not a Supplier? It does not throw any exceptions..
execute(() -> null);

This is because both the Callable<Void> method and the Supplier<T> method are applicable , but the former is more specific . You can see that this is the case by having only one of the two methods, and execute(() -> null); will call that method.

To show that execute(Callable<Void>) is more specific than execute(Supplier<T>) , we'll have to go to §18.5.4 , since the latter is a generic method.

Let m1 be the first method and m2 be the second method. Where m2 has type parameters P1, ..., Pp, let α1, ..., αp be inference variables, and let θ be the substitution [P1:=α1, ..., Pp:=αp].

Let e1, ..., ek be the argument expressions of the corresponding invocation. Then:

  • If m1 and m2 are applicable by strict or loose invocation (§15.12.2.2, §15.12.2.3), then let S1, ..., Sk be the formal parameter types of m1, and let T1, ..., Tk be the result of θ applied to the formal parameter types of m2.
  • [...]

So m1 is execute(Callable<Void>) , and m2 is execute(Supplier<T>) . P1 is T . For the invocation execute(() -> null); , e1 is () -> null , and T is inferred to be Object , so α1 is Object . T1 is then Supplier<Object> . S1 is Callable<Void> .

Now quoting only the parts relevant to the question:

The process to determine if m1 is more specific than m2 is as follows:

  • First, an initial bound set, B, is constructed from the declared bounds of P1, ..., Pp, as specified in §18.1.3.

  • Second, for all i (1 ≤ i ≤ k), a set of constraint formulas or bounds is generated.

    Otherwise, Ti is a parameterization of a functional interface, I. It must be determined whether Si satisfies the following five conditions:

    [...]

    If all five conditions are true, then the following constraint formulas or bounds are generated (where U1... Uk and R1 are the parameter types and return type of the function type of the capture of Si, and V1... Vk and R2 are the parameter types and return type of the function type of Ti):

    • If ei is an explicitly typed lambda expression:
      • [...]
      • Otherwise, ‹R1 <: R2›.

Note that a lambda with no parameters is an explicitly typed lambda.

Applying this back to your question, R1 is Void , R2 is Object , and the constraint ‹R1 <: R2› says that Void (not the lowercase void ) is a subtype of Object , which is correct and is not contradictory.

Finally:

Fourth, the generated bounds and constraint formulas are reduced and incorporated with B to produce a bound set B'.

If B' does not contain the bound false, and resolution of all the inference variables in B' succeeds, then m1 is more specific than m2.

Since the constraint ‹Void <: Object› is not contradictory, there is no false constraint, and so execute(Callable<Void>) is more specific than execute(Supplier<T>) .


// Supplier -> this returns an Object, but how is that different from returning null?
execute(() -> new Object());

In this case, only the Supplier<T> method is applicable . Callable<Void> expects you to return something compatible with Void , not Object .


// Callable -> because it can throw an exception, right?
execute(() -> {throw new Exception();});

Not quite . Throwing an exception made the Callable<Void> overload applicable, but the Runnable overload is still applicable too. The reason why the former is chosen is still because Callable<Void> is more specific than Runnable for the expression () -> { throw new Exception(); } () -> { throw new Exception(); } (relevant parts only):

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 (§15.27.1), then one of the following is true:
    • R2 is void .

Basically, any non- void -returning functional interface type is more specific than a void -returning functional interface type, for explicitly typed lambdas.

It all makes sense and has a simple pattern besides () -> null being a Callable I think. The Runnable is clearly different from the Supplier / Callable as it has no input and output values. The difference between Callable and Supplier is that with the Callable you have to handle exceptions.

The reason that () -> null is a Callable without an exception is the return type of your definition Callable<Void> . It requires you to return the reference to some object. The only possible reference to return for Void is null . This means that the lambda () -> null is exactly what your definition demands. It would also work for your Supplier example if you would remove the Callable definition. However, it uses Callable<Void> over Supplier<T> as the Callable has the exact type.

Callable is chosen over Supplier as it is more specific (as a comment already suggested). The Java Docs state that it chooses the most specific type if possible:

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

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