Java 如何知道要使用 lambda 表达式调用哪个重载方法?(供应商、消费者、可呼叫等)

2022-09-01 23:23:06

首先,我不知道如何体面地表达这个问题,所以这是有待建议的。

假设我们有以下重载方法:

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();
}

从这张桌子上走开,我从另一个SO问题中得到了

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

这些是我在本地得到的结果:

// 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();});

编译器如何知道要调用哪个方法?例如,它如何区分什么是a和什么是a?CallableRunnable


答案 1

我相信我已经找到了官方文档中描述的内容,尽管有点难以阅读。

这里提到:

15.27.3. Lambda 表达式的类型

请注意,虽然在严格调用上下文中不允许装箱,但始终允许对 lambda 结果表达式进行装箱 - 也就是说,无论包含 lambda 表达式的上下文如何,结果表达式都显示在赋值上下文中。但是,如果显式类型的 lambda 表达式是重载方法的参数,则最具体的检查 (§15.12.2.5) 首选避免装箱或取消装箱 lambda 结果的方法签名。

然后在这里(15.12.2.5)分析地描述了如何选择最具体的方法。

因此,根据这个例子描述

一个适用的方法 m1 比另一个适用的方法 m2 更具体,用于使用参数表达式 e1, ..., ek 的调用(如果满足以下任一条件):

m2 是通用的,对于参数表达式 e1, ..., ek,m1 被推断为比 m2 更具体

所以

// 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();
 }

为什么M1被推断得更具体,可以从这里描述的这个过程中追溯(18.5.4更具体的方法推断)


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

这是因为方法和方法都适用,但前者更具体。您可以看到,只有两个方法中的一个,并且将调用该方法,因此就是这种情况。Callable<Void>Supplier<T>execute(() -> null);

为了证明这比 更具体,我们必须转到 §18.5.4,因为后者是一个通用方法。execute(Callable<Void>)execute(Supplier<T>)

设 m1 是第一种方法,m2 是第二种方法。其中 m2 具有类型参数 P1, ..., Pp,设 α1, ..., αp 为推理变量,并设 θ 为替换 [P1:=α1, ..., Pp:=αp]。

设 e1, ..., ek 是相应调用的参数表达式。然后:

  • 如果 m1 和 m2 可通过严格或松散调用(§15.12.2.2, §15.12.2.3)适用,则设 S1, ..., Sk 是 m1 的形式参数类型,并让 T1, ..., Tk 是应用于 m2 的形式参数类型的 θ 的结果。
  • [...]

是 和 是 也是如此。 是。对于调用 ,是 ,并且被推断为是 ,也是 。 则为 。 是。m1execute(Callable<Void>)m2execute(Supplier<T>)P1Texecute(() -> null);e1() -> nullTObjectα1ObjectT1Supplier<Object>S1Callable<Void>

现在只引用与问题相关的部分:

确定 m1 是否比 m2 更具体的过程如下:

  • 首先,初始绑定集 B 是从 §18.1.3 中指定的 P1, ..., Pp 的声明边界构造的。

  • 其次,对于所有 i (1 ≤ i ≤ k),将生成一组约束公式或边界。

    否则,Ti 是函数接口 I 的参数化。必须确定Si是否满足以下五个条件:

    [...]

    如果所有五个条件都为真,则生成以下约束公式或边界(其中 U1 ...Uk和R1是Si捕获的函数类型的参数类型和返回类型,V1 ...Vk 和 R2 是 Ti 函数类型的参数类型和返回类型:

    • 如果 ei 是显式类型的 lambda 表达式:
      • [...]
      • 否则,‹R1 <:R2›。

请注意,没有参数的 lambda 是显式类型的 lambda。

将此应用回你的问题,是 ,is ,并且约束说(不是小写)是 的子类型,这是正确的并且不矛盾。R1VoidR2Object‹R1 <: R2›VoidvoidObject

最后:

第四,将生成的边界和约束公式简化并与 B 合并,以生成绑定集 B'。

如果 B' 不包含绑定 false,并且 B' 中所有推理变量的解析都成功,则 m1 比 m2 更具体。

由于约束不矛盾,因此没有约束,因此比 更具体。‹Void <: Object›falseexecute(Callable<Void>)execute(Supplier<T>)


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

在这种情况下,只有该方法适用。 希望您返回与 兼容的内容,而不是 。Supplier<T>Callable<Void>VoidObject


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

不完全是。引发异常使重载适用,但重载仍然适用。选择前者的原因仍然是因为它比表达式更具体(仅相关部分):Callable<Void>RunnableCallable<Void>Runnable() -> { throw new Exception(); }

对于表达式 e,如果功能接口类型 S 不是 S 的子类型并且以下之一为真(其中 U1 ...Uk和R1是捕获S的函数类型的参数类型和返回类型,V1 ...Vk 和 R2 是 T 的函数类型的参数类型和返回类型:

  • 如果 e 是显式类型的 lambda 表达式 (§15.27.1),则满足以下条件之一:
    • R2 是 。void

基本上,对于显式类型的 lambda,任何不返回的功能接口类型都比返回函数接口类型更具体。voidvoid