[英]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.