簡體   English   中英

使用 lambda 和泛型時對方法的引用不明確

[英]Reference to method is ambiguous when using lambdas and generics

我在以下代碼中遇到錯誤,我認為不應該在那里...使用 JDK 8u40 編譯此代碼。

public class Ambiguous {
    public static void main(String[] args) {
        consumerIntFunctionTest(data -> {
            Arrays.sort(data);
        }, int[]::new);

        consumerIntFunctionTest(Arrays::sort, int[]::new);
    }

    private static <T> void consumerIntFunctionTest(final Consumer<T> consumer, final IntFunction<T> generator) {

    }

    private static <T> void consumerIntFunctionTest(final Function<T, ?> consumer, final IntFunction<T> generator) {

    }
}

錯誤如下:

錯誤:(17, 9) java: 對consumerIntFunctionTest 的引用是不明確的,net.tuis.ubench.Ambiguous 中的方法consumerIntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction) 和方法consumerIntFunctionTest(java.util. function.Function,java.util.function.IntFunction) 在 net.tuis.ubench.Ambiguous 匹配

錯誤發生在以下行:

consumerIntFunctionTest(Arrays::sort, int[]::new);

我相信應該沒有錯誤,因為所有Arrays::sort引用都是void類型,並且它們都沒有返回值。 正如您所觀察到的,當我顯式擴展Consumer<T> lambda 時它確實有效。

這真的是 javac 中的錯誤,還是 JLS 聲明在這種情況下無法自動擴展 lambda? 如果是后者,我仍然認為這很奇怪,因為作為第一個參數Function<T, ?> consumerIntFunctionTest不應該匹配。

在你的第一個例子中

consumerIntFunctionTest(data -> {
        Arrays.sort(data);
    }, int[]::new);

lambda 表達式有一個void兼容塊,它可以通過表達式的結構來識別,而無需解析實際類型。

相比之下,在示例中

consumerIntFunctionTest(Arrays::sort, int[]::new);

必須解析方法引用以找出它是否符合void函數 ( Consumer ) 或值返回函數 ( Function )。 這同樣適用於簡化的 lambda 表達式

consumerIntFunctionTest(data -> Arrays.sort(data), int[]::new);

根據解析的目標方法,這可能是void兼容或值兼容的。

問題是解析該方法需要了解所需的簽名,這應該通過目標類型確定,但在通用方法的類型參數已知之前,目標類型是未知的。 雖然理論上兩者都可以同時確定,但規范中的(仍然非常復雜)過程已被簡化,因為首先執行方法重載解析,最后應用類型推斷(參見JLS §15.12.2 )。 因此,類型推斷可以提供的信息不能用於解決重載決議。

但請注意15.12.2.1 中描述的第一步。 確定可能適用的方法包括:

根據以下規則,表達式可能與目標類型兼容

  • 如果滿足以下所有條件,則 lambda 表達式(第 15.27 節)可能與函數接口類型(第 9.8 節)兼容:

    • 目標類型的函數類型的元數與 lambda 表達式的元數相同。

    • 如果目標類型的函數類型具有 void 返回值,則 lambda 主體是語句表達式(第 14.8 節)或與 void 兼容的塊(第 15.27.2 節)。

    • 如果目標類型的函數類型具有(非空)返回類型,則 lambda 主體是表達式或值兼容塊(第 15.27.2 節)。

  • 方法引用表達式(第 15.13 節)可能與函數接口類型兼容,如果在類型的函數類型數量為 n 的情況下,存在至少一種可能適用於數量為 n 的方法引用表達式(第 15.13.1 節)的方法,並且以下情況之一為真:

    • 方法引用表達式具有 ReferenceType :: [TypeArguments] 標識符的形式,並且至少一種可能適用的方法是 i) 靜態並支持 n 元數,或 ii) 非靜態且支持 n-1 元數。

    • 方法引用表達式有一些其他形式,並且至少一個潛在適用的方法不是靜態的。

潛在適用性的定義超出了基本的數量檢查,還考慮了功能接口目標類型的存在和“形狀”。 在某些涉及類型參數推斷的情況下,作為方法調用參數出現的 lambda 表達式在重載解析之后才能正確鍵入

因此,在第一個示例中,其中一個方法按 lambda 的形狀排序,而在方法引用或由單個調用表達式組成的 lambda 表達式的情況下,兩種可能適用的方法都會承受此第一次選擇過程並產生“模棱兩可”錯誤在類型推斷可以幫助查找目標方法以確定它是void還是值返回方法之前。

請注意,就像使用x->{ foo(); } x->{ foo(); }使lambda表達式明確void兼容,可以使用x->( foo() )使lambda表達式明確值兼容。


您進一步閱讀了這個答案,解釋說組合類型推斷和方法重載解析的這種限制是一個深思熟慮(但並不容易)的決定。

使用方法引用,您可以擁有完全不同的參數類型,更不用說返回類型了,如果您有另一個參數(參數數量)匹配的方法,仍然可以得到它。

例如:

static class Foo {

    Foo(Consumer<Runnable> runnableConsumer) {}

    Foo(BiFunction<Long, Long, Long> longAndLongToLong) {}
}

static class Bar {

    static void someMethod(Runnable runnable) {}

    static void someMethod(Integer num, String str) {}
}

Bar.someMethod()不可能滿足longAndLongToLong ,但下面的代碼發出相同的關於歧義的編譯錯誤:

new Foo(Bar::someMethod);

Holger 的回答很好地解釋了 JLS 中的邏輯和相關條款。

二進制兼容性怎么樣?

考慮Foo構造函數的longAndLongToLong版本是否不存在但后來在庫更新中添加,或者Bar.someMethod()的兩個參數版本不存在但后來添加:突然以前編譯的代碼可能會因為這個而中斷.

這是方法重載的一個不幸的副作用,甚至在 lambda 或方法引用出現之前,類似的問題就已經影響了普通的方法調用。

幸運的是,二進制兼容性得以保留。 相關條款在13.4.23 中。 方法和構造函數重載

添加重載現有方法或構造函數的新方法或構造函數不會破壞與預先存在的二進制文件的兼容性。 用於每個調用的簽名是在編譯這些現有二進制文件時確定的; ....

雖然添加新的重載方法或構造函數可能會在下次編譯類或接口時導致編譯時錯誤,因為沒有最具體的方法或構造函數(第 15.12.2.5 節),但當程序執行,因為在執行時沒有進行重載決議。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM