2014-02-20 24 views
36

我正在玩java8 lambda表達式,並且遇到了一個我沒有想到的編譯器錯誤。Java8:含lambda表達式和重載方法的模糊

說我有一個官能interface A,一個abstract class B並與採取任一AB作爲參數重載方法一個class C

public interface A { 
    void invoke(String arg); 
} 

public abstract class B { 
    public abstract void invoke(String arg); 
} 

public class C { 
    public void apply(A x) { }  
    public B apply(B x) { return x; } 
} 

然後我可以通過一個lambda成c.apply和其被正確地解析爲c.apply(A)

C c = new C(); 
c.apply(x -> System.out.println(x)); 

但是,當我改變需要B作爲參數傳遞給一個通用版本的編譯器報告的兩個重載是模糊的過載。

public class C { 
    public void apply(A x) { }  
    public <T extends B> T apply(T x) { return x; } 
} 

我以爲,編譯器會看到T必須的B一個子類,這是不是一個功能接口。爲什麼它不能解決正確的方法?

+6

我猜的答案是B的子類型牛逼可能實現A. –

+0

@ user2580516是的,這可能是問題,我沒有想到這種可能性。 – schenka7

回答

43

有很多複雜的在交叉路口重載分辨率和類型推斷。 lambda規範的current draft具有所有的細節。 F節和G節分別介紹重載分辨率和類型推斷。我不會假裝理解這一切。然而,引言中的總結部分是可以理解的,而且我建議人們閱讀它們,特別是F和G部分的摘要,以便了解這方面的情況。

爲了簡要回顧這些問題,請考慮在存在重載方法的情況下調用某些參數的方法。重載解析必須選擇正確的方法來調用。該方法的「形狀」(參數或參數數量)是最重要的;很顯然,帶有一個參數的方法調用無法解析爲帶有兩個參數的方法。但重載的方法通常具有相同數量的不同類型的參數。在這種情況下,類型開始變得重要。

假設有兩個重載方法:

void foo(int i); 
    void foo(String s); 

和一些代碼具有下列方法調用:

foo("hello"); 

顯然,這解決了第二方法,基於所述參數的是所述類型通過。但是如果我們正在做重載解析,而且參數是lambda? (尤其是那些類型是隱式的,依賴於類型推斷來建立類型的類型。)回想一下,lambda表達式的類型是從目標類型中推斷出來的,也就是這個上下文中預期的類型。不幸的是,如果我們有重載的方法,我們沒有一個目標類型,直到我們解決了我們要調用的重載方法。但是由於我們還沒有lambda表達式的類型,所以在重載解析期間我們不能使用它的類型來幫助我們。

讓我們來看看這裏的例子。考慮示例中定義的接口A和抽象類B。我們有一個包含兩個重載類C,然後是一些代碼調用apply方法,並傳遞一個lambda:

public void apply(A a)  
    public B apply(B b) 

    c.apply(x -> System.out.println(x)); 

兩個apply重載具有相同數量的參數。參數是一個lambda,它必須匹配一個功能接口。 AB是實際類型,因此它表明A是一個功能接口,而B不是,因此重載解析的結果是apply(A)。在這一點上,我們現在有一個針對lambda的目標類型A,並且類型推斷爲x

現在的變化:

public void apply(A a)  
    public <T extends B> T apply(T t) 

    c.apply(x -> System.out.println(x)); 

相反的實際類型,的apply第二過載是一個通用類型的變量T。我們還沒有進行類型推斷,因此我們不考慮T,至少在重載解析完成之後纔會這樣做。因此,兩種重載仍然適用,也不是最具體的,並且編譯器發出呼叫不明確的錯誤。

你可能會認爲,既然我們知道T結合的B一個類型,它是一類,而不是一個功能接口,在lambda不可能適用於這個過載,因此應該被排除在重載解析期間,消除歧義。我不是那個有爭論的人。 :-)這可能確實是編譯器或甚至規範中的錯誤。

我知道這個領域在Java 8的設計過程中經歷了一系列的變化。早期的變化確實試圖將更多的類型檢查和推理信息帶入重載解決階段,但是它們很難實現,並理解。 (是的,甚至比現在更難理解。)不幸的是問題不斷出現。決定通過減少可能超載的範圍來簡化事情。

類型推理和超載是反對的;從第1天開始,許多類型推斷的語言都禁止重載(除了可能在arity上。)因此,對於需要推理的隱式lambdas這樣的構造,似乎合理的是放棄一些重載力的東西來增加可以使用隱式lambdas的情況的範圍。

- Brian Goetz, Lambda Expert Group, 9 Aug 2013

(這是一個相當有爭議的決定需要注意的是有在這個線程116點的消息,並且還有其他一些討論這個問題的線程。)

之一這個決定的後果是某些API必須改變以避免超載,例如the Comparator API。此前,Comparator.comparing方法有四個重載:

comparing(Function) 
    comparing(ToDoubleFunction) 
    comparing(ToIntFunction) 
    comparing(ToLongFunction) 

的問題是,這些重載由拉姆達返回類型只能有區別的,我們其實從來沒有完全得到了類型推斷與隱式類型的lambda表達式在這裏工作。爲了使用這些,總是必須爲lambda轉換或提供顯式類型參數。這些API後來改爲:

comparing(Function) 
    comparingDouble(ToDoubleFunction) 
    comparingInt(ToIntFunction) 
    comparingLong(ToLongFunction) 

這是有點笨拙,但它是完全明確的。類似的情況Stream.mapmapToDoublemapToIntmapToLong,並在周圍的API其他一些地方發生。

底線是在存在類型推斷的情況下獲得重載分辨率是非常困難的,並且語言和編譯器設計人員爲了使類型推斷更好地工作,而將語言和編譯器設計人員從重載分辨率中分離出來。出於這個原因,Java 8 API避免了預計將使用隱式類型lambda的重載方法。

+1

謝謝您的全面解釋。我可以總結如下:當隱式類型的lambda表達式作爲參數傳遞時,通過重載解析不考慮泛型方法的類型參數的範圍? – schenka7

+1

我認爲這很公平。爲了擴展它,有很多信息在重載解析期間沒有考慮到。 –

4

我相信答案是B的子類型Ť可能實現A,由此使得含糊其功能是調度用於這種類型T的參數

+0

由於@StuartMarks評論了B的子類型,它實現了A不是一個功能接口。當我嘗試將lambda傳遞給使用這種類型參數化的方法時,編譯器報告錯誤。 – schenka7

1

我認爲這個測試案例暴露了一個情況,即使用javac 8的編譯器可以做更多的嘗試丟棄在不適合的超載候選人,第二個方法:基於這樣的事實

public class C { 
    public void apply(A x) { }  
    public <T extends B> T apply(T x) { return x; } 
} 

是T能夠永遠被實例化爲功能接口。這個案子非常有趣。 @ schenka7謝謝你問這個。我將調查這樣的建議的利弊。

眼下對實施這可能是此代碼是如何頻繁是主要的論點。我想,一旦人們開始將當前的Java代碼轉換爲Java 8,找到這種模式的可能性可能會更高。

另一個考慮是,如果我們開始添加特殊情況下的投機/編譯器它可以得到棘手理解,解釋和維護。

我已經申請了這個bug報告:JDK-8046045