簡體   English   中英

為什么Java 8泛型類型推斷會選擇此重載?

[英]Why does the Java 8 generic type inference pick this overload?

考慮以下程序:

public class GenericTypeInference {

    public static void main(String[] args) {
        print(new SillyGenericWrapper().get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(String string) {
        System.out.println("String");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

它在Java 8下打印“ String”,在Java 7下打印“ Object”。

我希望這在Java 8中是模棱兩可的,因為兩個重載方法都匹配。 為什么在JEP 101之后編譯器會選擇print(String)

是否合理,這破壞了向后兼容性,並且在編譯時無法檢測到更改。 升級到Java 8后,該代碼的行為便有所不同。

注意: SillyGenericWrapper被命名為“傻”是有原因的。 我試圖理解為什么編譯器會以這種方式運行,但不要告訴我愚蠢的包裝器最初是一個不好的設計。

更新:我也嘗試在Java 8下編譯並運行該示例,但使用Java 7語言級別。 該行為與Java 7一致。這是預料之中的,但是我仍然覺得有必要進行驗證。

在Java 8中,類型推斷規則已經得到了重大改進。 最值得注意的是,目標類型推斷已大大改進。 因此,盡管在Java 8之前,方法自變量站點沒有收到任何推斷,默認為Object,但在Java 8中,推斷了最特定的適用類型,在這種情況下為String。 JLS for Java 8引入了新的章節第18章。JLS for Java 7中缺少類型推斷

在早期版本的JDK 1.8(直到1.8.0_25之前)中,存在一個與重載方法解析有關的錯誤,當編譯器根據JLS成功編譯了代碼時,該代碼應產生歧義錯誤 正如Marco13在評論中指出的那樣

JLS的這一部分可能是最復雜的部分

解釋了早期版本的JDK 1.8中的錯誤以及您看到的兼容性問題。


如Java Tutoral( 類型推斷 )示例中所示

請考慮以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假設您要使用空列表調用方法processStringList。 在Java SE 7中,以下語句不會編譯:

processStringList(Collections.emptyList());

Java SE 7編譯器生成類似於以下內容的錯誤消息:

List<Object> cannot be converted to List<String>

編譯器需要類型參數T的值,因此它以值Object開頭。 因此,對Collections.emptyList的調用返回類型為List的值,該值與方法processStringList不兼容。 因此,在Java SE 7中,您必須指定type參數值的值,如下所示:

processStringList(Collections.<String>emptyList());

在Java SE 8中,這不再是必需的。什么是目標類型的概念已擴展為包括方法參數,例如processStringList方法的參數。 在這種情況下,processStringList需要一個類型為List的參數。

Collections.emptyList()是一種通用方法,類似於問題中的get()方法。 在Java 7中, print(String string)方法甚至不適用於方法調用,因此它不參與重載解析過程 而在Java 8中,這兩種方法均適用。

這種不兼容性在《 JDK 8兼容性指南》中值得一提。


您可以查看我的答案,以解決與重載方法解析相關的類似問題。Java 8三元條件和未裝箱原語的方法重載歧義

根據JLS 15.12.2.5,選擇最具體的方法

如果多個成員方法既可訪問又可應用於方法調用,則必須選擇一個成員方法來為運行時方法分派提供描述符。 Java編程語言使用選擇最具體方法的規則。

然后:

如果滿足以下任一條件,則使用參數表達式e1,...,ek進行調用時,一個適用的方法m1比另一適用的方法m2更具體:

  1. m2是通用的,並且對於第18.5.4節,對於參數表達式e1,...,ek推斷m1比m2更具體。

  2. m2不是通用的,並且m1和m2可通過嚴格調用或寬松調用來應用,並且m1具有形式參數類型S1,...,Sn,而m2具有形式參數類型T1,...,Tn,則Si類型更多對於所有i(1≤i≤n,n = k),自變量ei比Ti特定。

  3. m2不是通用的,並且m1和m2可通過可變arity調用來應用,並且m1的前k個可變arity參數類型為S1,...,Sk,而m2的前k個可變arity參數類型為T1,...。對於所有i(1≤i≤k),自變量ei的類型Si比Ti更具體。 另外,如果m2具有k + 1個參數,則m1個第k + 1個可變稀疏參數類型是m2個第k + 1個可變稀疏參數類型的子類型。

以上條件是一種方法可能比另一種方法更具體的唯一情況。

如果S <:T(第4.10節),則對於任何表達式,類型S比類型T更具體。

三個選項中的第二個匹配我們的情況。 因為StringObject的子類型( String <: Object ),所以它更加具體。 因此,該方法本身更加具體 在JLS之后,此方法也嚴格更具體,並且最具體 ,由編譯器選擇。

在java7中,表達式是自下而上地解釋的(很少有例外)。 子表達式的含義是“無上下文”。 對於方法調用,首先要解析參數的類型。 然后,編譯器使用該信息來解析調用的含義,例如,從適用的重載方法中選擇一個贏家。

在java8中,該原理不再起作用,因為我們希望在所有地方都使用隱式lambda(例如x->foo(x) )。 未指定lambda參數類型,必須從上下文中推斷出來。 這意味着,對於方法調用,有時方法參數類型決定參數類型。

如果方法被重載,顯然存在一個難題。 因此,在某些情況下,有必要在編譯參數之前先解決方法重載以選擇一個獲勝者。

這是一個重大轉變。 並且某些像您一樣的舊代碼將成為不兼容的受害者。

一種解決方法是為帶有“廣播上下文”的參數提供“目標類型”

    print( (Object)new SillyGenericWrapper().get() );

或像@Holger的建議一樣,提供類型參數<Object>get()以避免一起推斷。


Java方法重載非常復雜; 復雜性的好處令人懷疑。 請記住,重載從來沒有必要-如果它們是不同的方法,則可以為它們指定不同的名稱。

首先,它與覆蓋無關,但必須處理重載。

Jls, 第15節提供了許多有關編譯器如何正確選擇重載方法的信息。

在編譯時選擇最具體的方法。 它的描述符確定在運行時實際執行哪種方法。

所以當調用

print(new SillyGenericWrapper().get());

編譯器選擇String版本過Object ,因為print是需要方法String是更具體的,則一個需要Object 如果有Integer而不是String那么它將被選中。

此外,如果您要調用以Object作為參數的方法,則可以將返回值分配給類型為object Eg的參數

public class GenericTypeInference {

    public static void main(String[] args) {
        final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
        final Object o = sillyGenericWrapper.get();
        print(o);
        print(sillyGenericWrapper.get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(Integer integer) {
        System.out.println("Integer");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

輸出

Object
Integer

假設您有2個可以重載的有效方法定義,這種情況開始變得有趣起來。 例如

private static void print(Integer integer) {
    System.out.println("Integer");
}

private static void print(String integer) {
    System.out.println("String");
}

現在,如果您調用

print(sillyGenericWrapper.get());

編譯器將有2種有效的方法定義可供選擇,因此,您將得到編譯錯誤,因為它無法優先選擇一種方法。

我使用Java 1.8.0_40運行了它,並得到了“對象”。

如果您將運行以下代碼:

public class GenericTypeInference {

private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {

    print(new SillyGenericWrapper().get());

    Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
    for (Method m : allMethods) {
        System.out.format("%s%n", m.toGenericString());
        System.out.format(fmt, "ReturnType", m.getReturnType());
        System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());   
   }

   private static void print(Object object) {
       System.out.println("Object");
   }

   private static void print(String string) {
       System.out.println("String");
   }

   public static class SillyGenericWrapper {
       public <T> T get() {
           return null;
       }
   }
}

您會看到得到:

Object public T com.xxx.GenericTypeInference $ SillyGenericWrapper.get()ReturnType:類java.lang.Object GenericReturnType:T

這就解釋了為什么使用Object重載而不是String重載的方法。

暫無
暫無

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

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