[英]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三元條件和未裝箱原語的方法重載歧義
如果多個成員方法既可訪問又可應用於方法調用,則必須選擇一個成員方法來為運行時方法分派提供描述符。 Java編程語言使用選擇最具體方法的規則。
然后:
如果滿足以下任一條件,則使用參數表達式e1,...,ek進行調用時,一個適用的方法m1比另一適用的方法m2更具體:
m2是通用的,並且對於第18.5.4節,對於參數表達式e1,...,ek推斷m1比m2更具體。
m2不是通用的,並且m1和m2可通過嚴格調用或寬松調用來應用,並且m1具有形式參數類型S1,...,Sn,而m2具有形式參數類型T1,...,Tn,則Si類型更多對於所有i(1≤i≤n,n = k),自變量ei比Ti特定。
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更具體。
三個選項中的第二個匹配我們的情況。 因為String
是Object
的子類型( 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.