[英]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 形式参数类型的结果。
- [...]
所以m1
是execute(Callable<Void>)
, m2
是execute(Supplier<T>)
。 P1
是T
。 对于调用execute(() -> null);
, e1
为() -> null
, T
推断为Object
, 所以α1
为Object
。 T1
然后是Supplier<Object>
。 S1
是Callable<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。
将此应用回您的问题, R1
是Void
, R2
是Object
,并且约束‹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 值。 Callable
和Supplier
之间的区别在于,对于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.