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