繁体   English   中英

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

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

首先,我不知道如何恰当地表达这个问题,所以这是征求意见。

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

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 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

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

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

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

我相信我已经找到了官方文档中对此进行描述的位置,尽管有点难以阅读。

这里提到:

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 更具体的方法推断)

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

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

为了表明execute(Callable<Void>)execute(Supplier<T>)更具体,我们必须从 go 到§18.5.4 ,因为后者是通用方法。

令 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>) P1T 对于调用execute(() -> null); , e1() -> null , T推断为Object , 所以α1Object T1然后是Supplier<Object> S1Callable<Void>

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

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

  • 首先,初始边界集 B 是根据声明的边界 P1, ..., Pp 构建的,如 §18.1.3 中所述。

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

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

    [...]

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

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

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

将此应用回您的问题, R1VoidR2Object ,并且约束‹R1 <: R2›表示Void (不是小写的void )是Object的子类型,这是正确的并且并不矛盾。

最后:

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

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

由于约束‹Void <: Object›并不矛盾,因此不存在false约束,因此execute(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>期望您返回与Void兼容的内容,而不是Object


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

完全是 抛出异常使Callable<Void>重载适用,但Runnable重载仍然适用。 之所以选择前者,还是因为Callable<Void>对于表达式() -> { throw new Exception(); }Runnable更具体。 () -> { throw new Exception(); } (仅相关部分):

如果 T 不是 S 的子类型并且以下条件之一为真(其中 U1...Uk 和 R1 是参数类型和返回类型),则功能接口类型 S 比表达式 e 的功能接口类型 T 更具体S的捕获的function类型,V1...Vk和R2是T的function类型的参数类型和返回类型:

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

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

这一切都是有道理的,并且除了() -> null之外还有一个简单的模式,我认为它是一个Callable Runnable明显不同于Supplier / Callable ,因为它没有输入和 output 值。 CallableSupplier之间的区别在于,对于Callable ,您必须处理异常。

() -> null是 Callable 无一例外的原因是您定义的返回类型Callable<Void> 它要求您返回对某些 object 的引用。返回Void的唯一可能引用是null 这意味着 lambda () -> null正是您的定义所要求的。 如果您删除Callable定义,它也适用于您的Supplier示例。 但是,它使用Callable<Void>而不是Supplier<T> ,因为Callable具有确切的类型。

选择Callable而不是Supplier因为它更具体(正如已经建议的评论)。 Java Docs state 它尽可能选择最具体的类型:

类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数(或参数)的能力。 推理算法确定 arguments 的类型,如果可用,则确定分配或返回结果的类型。 最后,推理算法尝试找到适用于所有 arguments 的最具体的类型。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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