Java语言 泛型 类型擦除
初學(xué)者只要學(xué)習(xí)了"Java 編程簡介學(xué)習(xí)路徑"的第 20 單元,也可以學(xué)習(xí)本文。
此文將定義類型擦除,它與 Java 泛型的關(guān)系,以及未正確使用泛型時看到的一些神秘錯誤和警告消息(相信我,我們都經(jīng)歷過這些)。
前提條件
Java 編程語言的基礎(chǔ)知識
- 更多信息:?《Java 編程簡介》學(xué)習(xí)路徑,第 1 單元
Java 泛型的初中級知識(第 20 單元)
- 更多信息:?《Java 編程簡介》學(xué)習(xí)路徑,第 3 單元
逐步介紹
設(shè)置開發(fā)環(huán)境
要完成此文,需要安裝 JDK 和 Eclipse IDE。假設(shè)您擁有一定的 Java 基礎(chǔ)知識。如果沒有,請查閱 IBM developerWorks 上的?《Java 編程簡介》學(xué)習(xí)路徑?。
您還需要一個正常工作的開發(fā)環(huán)境。如果已有一個 Java 開發(fā)環(huán)境,可跳到第 2 步。
否則,請參閱?《Java 編程簡介》學(xué)習(xí)路徑,第 2 單元?獲得逐步操作說明。如果需要更多幫助,本節(jié)中還有一些視頻可幫助您。
首先,下載 Java Development Kit (JDK) V8 并將它安裝在您的機(jī)器上。如果需要幫助,請觀看下面的視頻。
點(diǎn)擊查看視頻演示
?接下來,將 Eclipse IDE 安裝在計(jì)算機(jī)上。如果需要幫助,請觀看下面的視頻。
點(diǎn)擊查看視頻演示
設(shè)置并準(zhǔn)備好開發(fā)環(huán)境后,前進(jìn)到第 2 步,這一步將定義類型擦除。
定義類型擦除
我們在編寫 Java 代碼時都會犯錯,在犯錯時,Java 編譯器會提供警告和錯誤消息。但有時,從 Java 編譯器獲得的信息有些晦澀難懂,尤其是在使用 Java 泛型時(除非您已了解類型擦除)。
在此文中,將會展示您將看到的與 Java 泛型相關(guān)的一些最常見警告和錯誤,以及如何避免或修復(fù)它們。首先,我們需要定義泛型工作原理背后的重要概念,那就是類型擦除。
類型擦除是 Java 編譯器用來支持使用泛型的一項(xiàng)技術(shù)。在?《Java 編程簡介》學(xué)習(xí)路徑的第 20 單元中,我展示了如何使用 Java 泛型,您已在其中了解了如何創(chuàng)建參數(shù)化的類和方法。我沒有真正談?wù)擃愋筒脸?#xff0c;因?yàn)樗且粋€非常復(fù)雜的主題,而且如果正確使用 Java 泛型,實(shí)際上不需要理解它。
如果編寫的 Java 代碼足夠長,就會看到我將展示的部分或所有消息。完成此文 后,您應(yīng)能理解這些消息的含義,以及如何永遠(yuǎn)避免它們!
使用泛型定義參數(shù)化的類時,Java 編譯器不會實(shí)際創(chuàng)建一個新類型(出于各種深層的技術(shù)原因,這里不會詳細(xì)解釋)。編譯器會接受您指定的 類型,將它擦除并替換回以下兩種類型之一:上限(如果您已指定)或 Object (如果沒指定)。考慮這個示例:
| 1 2 3 4 5 6 7 8 9 | public class ObjectContainer<T> { ????private T contained; public ObjectContainer(T contained) { ????this.contained = contained; } public T? getContained() { ????return contained; } } |
在這個示例中,聲明參數(shù)化類型 ObjectContainer 時未指定上限,所以編譯器生成以下代碼:
| 1 2 3 4 5 6 7 8 9 | public class ObjectContainer { ????private Object contained; public ObjectContainer(Object contained) { ????this.contained = contained; } public Object getContained() { ????return contained; } } |
因?yàn)闆]有上限,參數(shù)的類型 ( T ) 被擦除并替換回 Object 。在聲明 ObjectContainer 時,編譯器插入了一個強(qiáng)制轉(zhuǎn)換,所以代碼類似于:
| 1 2 3 | ObjectContainer<Person> personContainer = new ObjectContainer<>(new Person("Steve", 49)); ?????????????????????????????Person contained = personContainer.getContained(); System.out.println("ObjectContainer<Person> contains: " +contained.toString()); |
但是,編譯器生成以下代碼:
| 1 2 3 | ObjectContainer personContainer = new ObjectContainer(new Person("Steve",49)); ???????????????Person contained = (Person)personContainer.getContained(); System.out.println("ObjectContainer<Person> contains: " +contained.toString()); |
請注意上面代碼中轉(zhuǎn)換為 Person 的強(qiáng)制轉(zhuǎn)換。這是因?yàn)?#xff0c;編譯器在幕后將聲明的類型 ( Person ) 擦除并替換回 Object ,必須插入強(qiáng)制轉(zhuǎn)換,代碼才能正確運(yùn)行。
使用限定類型時,就會出現(xiàn)類似情況,除非使用指定的上限,而不是使用 Object 作為上限。
考慮下面的代碼:
| 1 2 3 4 5 6 7 8 9 | public class ObjectContainer<T extends Person> { ????private T contained; public ObjectContainer(T contained) { ????this.contained =contained; } public T getContained() { ????return contained; } } |
在這種情況下,編譯器生成以下代碼:
| 1 2 3 4 5 6 7 8 9 | public class ObjectContainer { ???private Person contained; public ObjectContainer(Person contained) { ???this.contained = contained; } public Person getContained() { ???return contained; } } |
擦除參數(shù)的類型 ( T extends Person ) 并替換回 Person ,后者是上限。聲明 ObjectContainer<Employee> 時,編譯器插入了一個強(qiáng)制轉(zhuǎn)換,所以代碼類似于:
| 1 2 3 | ObjectContainer<Employee> personContainer = new ObjectContainer<>(new Employee("Steve", 49, "EMP001")); ?????????????????????????????Employee contained = personContainer.getContained(); System.out.println("ObjectContainer<Employee> contains: " + contained.toString()); |
但是,編譯器生成以下代碼:
| 1 2 3 | ObjectContainer<Employee> personContainer = new ObjectContainer<>(new Employee("Steve", 49, "EMP001")); ???????????????Employee<String> contained = (Employee)personContainer.getContained(); System.out.println("ObjectContainer<Employee> contains: " + contained.toString()); |
解決錯誤
泛型通常很容易使用。除了在個別情況下。在我的經(jīng)驗(yàn)中,當(dāng)我嘗試執(zhí)行從面向?qū)ο蠼嵌戎v合理、但不受泛型支持的操作(通常為?協(xié)變?)時,就會出現(xiàn)這種情況。
接下來的 3 節(jié)將介紹兩種錯誤和一種警告,如果您使用的泛型足夠長,肯定會在某處看到這些錯誤和警告。看到它們后,您就會知道如何修復(fù)問題。
首先介紹錯誤。遇到這些錯誤時(您一定會遇到),您要知道發(fā)生了什么,這樣才能修復(fù)它們。
然后將介紹比任何其他與泛型相關(guān)的警告更常見的警告。遇到此警告時(您一定會遇到),您就會知道該做什么。
錯誤 1 - “Erasure of method xyz(…) is the same as another method in type Abc”
考慮下面的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class App { public int process(List<Person> people) { ????for (Person person : people) { ????log.info("Processing person: " + person.toString()); } ????return person.size(); } public int process(List<Employee> employees) { ????for (Employee employee :employees) { ????log.info("Processing employee: " + employee.toString()); } ????return employees.size(); } } |
上面的代碼初看起來很正常(比如 process() 只一個重載的方法),但是當(dāng)您編譯它時,會獲得以下消息:
Erasure of method process(List<Person>) is the same as another method in type App Erasure of method process(List<Employee>) is the same as another method in type App
發(fā)生了什么?這些方法有不同的方法簽名,所以重載了 process() 方法,對嗎?不對。回憶一下第 2 步,使用泛型時,(在本例中)編譯器擦除了 <> 中指定的類型并替換回 Object 。編譯器生成的代碼類似于:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class App { public int process(List people) { ????for (Person person : people) { ????log.info("Processing person: " + person.toString()); } ????return person.size(); } public int process(List employees) { ????for (Employee employee : employees) { ????log.info("Processing employee: " + employee.toString()); } ????return employees.size(); } } |
現(xiàn)在,存在的問題顯而易見。兩個具有相同簽名 ( process(List) ) 的方法無法在同一個類中共存。
知道編譯器如何擦除類型后,可以稍微更改一下設(shè)計(jì)來修復(fù)該問題:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class App { public int processPeople(List<Person>people) { ????for (Person person : people) { ????log.info("Processing person: " + person.toString()); } ????return person.size(); } public int processEmployees(List<Employee> employees) { ????for (Employee employee : employees) { ????log.info("Processing employee: " + employee.toString()); } ????return employees.size(); } } |
現(xiàn)在,代碼能正常編譯,方法名稱更準(zhǔn)確地反映了它們實(shí)際執(zhí)行的操作。
錯誤 2 - “The method xyz(Foo) in the type Abc is not applicable for the arguments (Foo)”
通常,會在以下情況下看到此錯誤: A 是 B 的超類,而且似乎可以合理地假設(shè)泛型類型 Foo<B> 是 Foo<A> 的子類(或者行為上類似子類,也就是說,具有協(xié)變行為)。但是,Java 泛型沒有協(xié)變性,可以認(rèn)為盡管 B 是 A 的子類,但 SomeGenericType<B> 既不是 SomeGenericType<A> 的子類,行為也不像子類。
基本上講,此錯誤與我們已在方法名稱上看到的錯誤非常相似,但此錯誤適用于方法參數(shù)。基礎(chǔ)問題相同。
考慮下面的代碼(備注: Employee 是 Person 的子類):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class App { public int processPeople(List<Person> people){ ????for (Person person : people) { ????log.info("Processing person: " + person.toString()); } ????return person.size(); } .. } .. ????List<Employee>employees; ????employees = new ArrayList<>(); ????employees.add(employee1); ????employees.add(employee2); ????App app = new App(); ????// ERROR ON NEXT LINE! ????app.processPeople(employees);.. |
對 App.processPeople(List<Employee>) 的調(diào)用生成以下錯誤消息:
The method processPeople(List<Person>) in the type App is not applicable for the arguments
(List<Employee>)
最初,這似乎是合理的,因?yàn)?Employee 是 Person 的子類,我們可以將一個 List<Employee> 傳遞給一個需要 List<Person> 的方法,對嗎?
不對。由于類型擦除, List<Person> 和 List<Employee> 被擦除并替換回 List 。(在我看來)該消息讓人困惑,而且應(yīng)提及錯誤的擦除方面(就像我們看到的第一個錯誤一樣)。
因?yàn)橐巡脸擃愋筒⑻鎿Q回 Object ,所以您可能認(rèn)為編譯器會允許這種情況通過檢測。但事實(shí)是,編譯器知道,由于類型擦除, List<Employee> 不是 List<Person> 的合適替代。擦除類型后,有關(guān)實(shí)際參數(shù)類型的信息就會丟失,而且允許編譯此代碼會導(dǎo)致運(yùn)行時問題。
那么如何修復(fù)此問題?也可以專門使用(或創(chuàng)建)一個方法來處理(之前示例中的) Employee 。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class App { public int processPeople(List<Person> people){ ????for (Person person : people) { ????log.info("Processing person: " + person.toString()); ?} ????return person.size(); } public int processEmployees(List<Employee> employees) { ????for (Employee employee : employees) { ????log.info("Processing employee: " + employee.toString()); } ????return employees.size(); } }.. ????List<Employee> employees; ????employees = new ArrayList<>(); ????employees.add(employee1); ????employees.add(employee2); ????App app = new App(); ?// ERROR ON NEXT LINE! ????app.processEmployees(employees);.. |
?但是,如果沒有 processEmployees() 方法,而且 Person 與 Employee 之間實(shí)際共享了"process person"的邏輯,該怎么辦?可以將 processPeople() 的簽名更改為:
| 1 2 3 4 5 6 7 8 9 10 11 | public int processPeople(List<? extends Person> people)... . . . List<Employee> employees; employees = new ArrayList<>(); employees.add(employee1); employees.add(employee2); App app = new App();? // THIS WORKS GREAT NOW! app.processPeople(employees); |
現(xiàn)在,編譯器認(rèn)為類型參數(shù)的上限為 Person ,并在它生成的代碼中使用 Person (而不是 Object ),而且代碼運(yùn)行正常。
警告 - “Foo is a raw type.References to generic type Foo should be parameterized”
警告不會阻止程序運(yùn)行,但獲得警告就表明代碼中的某處可能存在錯誤。看到類似這樣的警告時,知道發(fā)生了什么會對您有所幫助,這樣您就可以知道代碼是正常的還是會在某個時刻引發(fā)問題。
泛型被設(shè)計(jì)為向后兼容原始類型(例如,對 List<T> 的引用兼容 List )。但是,您編寫的任何使用泛型的新代碼都絕不應(yīng)該使用原始類型。
為什么?像這樣通過引用在非參數(shù)化的泛型類型上調(diào)用方法是很危險(xiǎn)的。它可能導(dǎo)致?堆污染?等問題,我稍后將展示。
考慮下面的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class ObjectContainer<T> { ????private T contained; public ObjectContainer(T contained) { ????this.contained = contained; } public T getContained() { ????return contained; } public void setContained(T contained){ ????this.contained = contained; } @Override public String toString() { ????return contained.toString(); } } ... public class PersonContainer extends ObjectContainer<Person> { public PersonContainer(Person contained) { ????super(contained); } @Override public void setContained(Person contained) { ????super.setContained(contained); } } .. PersonContainer pc = new PersonContainer(new Person("Test", 23)); ????ObjectContainer oc = pc; // WARNING occurs here System.out.println("PersonContainer (through ObjectContainer): " + oc.toString()); |
我引用的警告出現(xiàn)在我指定的位置上面的行上。在這里,準(zhǔn)確的警告是:
ObjectContainer is a raw type.References to generic type ObjectContainer<T> should be?
parameterized
使用原始泛型類型時,可能發(fā)生糟糕的事情。您可能想知道會發(fā)生哪些糟糕的事情。請繼續(xù)閱讀。
考慮下面這段代碼(它是在上一節(jié)的 ObjectContainer 定義的基礎(chǔ)上構(gòu)建的):
由于類型擦除, PersonContainer 不再通過 setContained() 方法來獲得多態(tài)性。請記住,泛型類型被擦除并替換回它的上限,所以 ObjectContainer 實(shí)際上看起來類似于:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ObjectContainer { ????private Object contained; public ObjectContainer(Object contained) { ????this.contained = contained; } public Object getContained() { ????return contained; } public void setContained(Object contained) { ????this.contained = contained; } @Override public String toString() { ????return contained.toString(); } } |
目前一切順利,但問題仍然存在,因?yàn)槲以?PersonContainer 中提供了一個接受 Person 對象的 setContained() 版本。現(xiàn)在, PersonContainer 和它的超類 ObjectContainer 之間的 setContained() 簽名是不同的。初看起來,似乎重寫了 setContained() ,事實(shí)并非如此。
為了保留多態(tài)性,編譯器為 PersonContainer 生成 setContained()?橋接方法?的重寫版本,所以 PersonContainer 實(shí)際上看起來類似于:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class PersonContainer extends ObjectContainer<Person> { public PersonContainer(Person contained) { ????super(contained); ?} //Bridge method generated by the compiler – you never see this method //(unless there is a problem) public void setContained(Object contained) { ????setContained((Person)contained); } @Override public void setContained(Person contained) { ????super.setContained(contained); } } |
現(xiàn)在考慮如果運(yùn)行此代碼會發(fā)生什么 - 假設(shè)我將它放在一個測試方法中:
| 1 2 3 4 5 6 7 8 9 | @Test @DisplayName("Testing PersonContainer - will throw ClassCastException") public void testSetContainedPerson() { ????????????????PersonContainer pc = new PersonContainer(new Person("Test", 23)); ????????????????ObjectContainer oc = pc; // WARNING occurs here ????????????????System.out.println("PersonContainer (through ObjectContainer): " + oc.toString()); ?// ClassCastException.Not good.assertThrows(ClassCastException.class, () -> oc.setContained("Howdy!")); } |
這就是我們所說的堆污染。堆污染不是好事。
類型擦除支持泛型類型的向后兼容,但如果未正確使用泛型,可能導(dǎo)致各種各樣的煩人問題。
現(xiàn)在您已更深入地了解了類型擦除,在遇到與泛型相關(guān)的錯誤和警告消息時,您將能更好地處理它們。
使用泛型時的最佳經(jīng)驗(yàn)規(guī)則是:堅(jiān)決不讓泛型相關(guān)警告悄然存在。編譯器會提醒您未正確使用泛型,而且您應(yīng)認(rèn)真留意到這些警告。
解決錯誤(視頻)
我創(chuàng)建了一個視頻來演練前幾節(jié)中的代碼,指出我們看到的各種錯誤,以及您不當(dāng)使用泛型時,編譯器為了提醒"危險(xiǎn)"而發(fā)出的一些警告。
在該視頻中,我展示了如何:
- 克隆包含此 recipe 的代碼的 Github 存儲庫。
- 將基于 Github 中的代碼的新 Maven 項(xiàng)目導(dǎo)入 Eclipse 中。
- 演示:
-
- 錯誤 1 和錯誤 2,
- 忽略警告時會發(fā)生什么(糟糕的事情!)
- 一個額外警告,如果忽略該警告,則會發(fā)生其他糟糕的事情!
點(diǎn)擊查看視頻演示
后續(xù)行動
網(wǎng)上與類型擦除和 Java 泛型相關(guān)的資源有許多。本節(jié)給出了我最喜歡的一些資源。盡情閱讀吧!
Angelika Langer – 類型擦除
Oracle 文檔 – 類型擦除
相關(guān)主題
- 了解泛型
from:https://www.ibm.com/developerworks/cn/java/java-language-type-erasure/index.html
總結(jié)
以上是生活随笔為你收集整理的Java语言 泛型 类型擦除的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java泛型:类型擦除
- 下一篇: Java泛型的类型擦除