[英]Why does this program involving a generic interface method compile?
以下代碼在運行時拋出ClassCastException
,並且行public String foo() { return "bar"; }
public String foo() { return "bar"; }
生成一個警告“找到‘java.lang.String中’,需要‘T’”。 我理解ClassCastException
(調用接口方法, T
等於Integer
但foo
返回一個String
),我理解警告(它試圖警告我們這個問題)。 但我不明白的是程序編譯的原因。 為什么返回String
的方法允許覆蓋返回T
的方法?
public class Main {
interface MyInterface {
<T> T foo();
}
static class MyClass implements MyInterface {
@Override
public String foo() { return "bar"; }
}
public static void main(String[] args) {
int a = ((MyInterface) new MyClass()).<Integer>foo();
}
}
當天真地宣布<T> T foo();
編譯器將嘗試從將被賦值的變量中推斷出foo
的結果類型。 這就是編譯的原因。 它可以很容易地測試:
interface MyInterface {
<T> T foo();
}
class MyClass implements MyInterface {
@Override
public String foo() { return "bar"; }
}
public class Main {
public static void main(String[] args) {
MyInterface myInterface = new MyClass();
//the result of foo will be String
String bar = myInterface.foo();
System.out.println(bar); //prints "bar"
try {
//the result of foo at compile time will be Integer
Integer fail = myInterface.foo();
System.out.println(fail); //won't be executed
} catch (ClassCastException e) {
//for test purposes only. Exceptions should be managed better
System.out.println(e.getMessage()); //prints "java.lang.String cannot be cast to java.lang.Integer"
}
}
}
並且編譯時的結果不能是Object
。 如果它是對象,那么你將不得不添加一個手動類型轉換,但事實並非如此。
簡而言之,聲明這樣的方法是無用的,只會給程序員帶來混亂和混亂。
此方法聲明在以下情況之一中變得有用:
在接口/類的頂級聲明泛型<T>
時:
interface MyInterface<T> { T foo(); } class MyClass implements MyInterface<String> { @Override //can only return String here. Compiler can check this public String foo() { return "bar"; } }
傳遞Class<T>
作為參數時,它使編譯器能夠在不滿足此條件時推斷結果類型並引發正確的編譯器錯誤:
interface MyInterface { <T> T foo(Class<T> clazz); } class MyClass implements MyInterface { @Override public <T> T foo(Class<T> clazz) { try { return clazz.newInstance(); } catch (InstantiationException e) { e.printStackTrace(System.out); } catch (IllegalAccessException e) { e.printStackTrace(System.out); } return null; } } public class Main { public static void main(String[] args) { MyInterface myInterface = new MyClass(); //uncomment line below to see the compiler error //Integer bar = myInterface.foo(String.class); //line below compiles and runs with no problem String bar = myInterface.foo(String.class); System.out.println(bar); } }
因為聲明方法<T> T foo()
與聲明Object foo()
基本相同。 如果你在某個地方有類型參數的其他連接(也許接口在T
參數化,方法只是T foo()
),那么可能存在某種可能違反的鏈接。 但是,在這種情況下,您只是回到標准規則,即覆蓋可以返回超類型返回的任何更具體的子類型。
這是一個非常深刻的問題。 Java語言規范寫道 :
一個實例方法mC
中聲明或由類繼承C
,從覆蓋C
另一方法mA
類聲明A
,當且僅當滿足以下所有條件都為真:
mC
的簽名是mA
簽名的子簽名(§8.4.2)。 並且 :
兩個方法或構造函數
M
和N
具有相同的簽名,如果它們具有相同的名稱,相同的類型參數(如果有的話)(第8.4.4節),並且在將形式參數類型N
調整為類型參數之后M
,相同的形式參數類型。
在我們的例子中顯然不是這樣,因為MyInterface.foo
聲明了一個類型參數,但是MyClass.foo
沒有。
方法
m1
的簽名是方法m2
的簽名的子簽名,如果:
m2
與m1
具有相同的簽名,或m1
的簽名與m2
簽名的擦除(§4.6)相同。
該規范解釋了第二個條件的必要性如下:
子簽名的概念旨在表達兩種方法之間的關系,這兩種方法的簽名不相同,但可以覆蓋另一種方法。 具體來說,它允許其簽名不使用泛型類型的方法覆蓋該方法的任何泛化版本。 這很重要,因此庫設計者可以獨立於定義庫的子類或子接口的客戶端自由地生成方法。
事實上,在我們的情況下滿足第二個條件,因為MyClass.foo
具有簽名foo()
,這也是MyInterface.foo
簽名的MyInterface.foo
。
這留下了不同回報類型的問題。 規范寫道 :
如果具有返回類型
R1
的方法聲明d1
覆蓋或隱藏具有返回類型R2
的另一個方法d2
的聲明,則對於d2
,d1
必須是return-type-substitutable(第8.4.5節),否則會發生編譯時錯誤。
並且 :
返回類型為R1的方法聲明d1是另一個方法d2的return-type-substitutable,返回類型為R2 iff以下任何一個為真:
...
如果R1是引用類型,則滿足以下條件之一:
適應d2(§8.4.4)類型參數的R1是R2的子類型。
可以通過未經檢查的轉換將R1轉換為R2的子類型(第5.1.9節)。
d1與d2(§8.4.2)沒有相同的簽名,R1 = | R2 |。
在我們的例子中,R1 = String和R2 = T.因此,第一個條件是false,因為String不是T的子類型。但是,String可以通過未經檢查的轉換轉換為T,使第二個條件成立。
該規范解釋了第二和第三個條件的必要性如下:
盡管不健全,但定義中允許進行未經檢查的轉換,作為允許從非泛型代碼平滑遷移到通用代碼的特殊容差。 如果使用未經檢查的轉換來確定R1是R2的返回類型可替換,那么R1必然不是R2的子類型,並且覆蓋規則(§8.4.8.3,§9.4.1)將需要編譯時未經檢查的警告。
也就是說,編譯器接受了您的代碼,因為您無意中使用了兩個引入的編譯器功能,以便通過允許逐步生成現有代碼來簡化向泛型的轉換。 這些功能在編譯時類型系統中打開了一個循環漏洞,這可能會導致堆污染和奇怪的ClassCastExceptions行甚至可能在源代碼中沒有強制轉換。 為了提醒您這種危險,編譯器需要發出未經檢查的警告。 因此,這些特征僅應用於其預期目的(與非通用遺留代碼的兼容性),否則應避免使用。
由於類型擦除, T
部分只是字節代碼中的一個Object
。 您可以返回更具體的類型,在本例中為String
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.