原文網(wǎng)址:http://www.cnblogs.com/xltcjylove/p/3671943.html 一,、萬惡的擦除 我在自己總結(jié)的【Java心得總結(jié)三】Java泛型上——初識泛型這篇博文中提到了Java中對泛型擦除的問題,考慮下面代碼: 1 import java.util.*; 2 public class ErasedTypeEquivalence { 3 public static void main(String[] args) { 4 Class c1 = new ArrayList<String>().getClass(); 5 Class c2 = new ArrayList<Integer>().getClass(); 6 System.out.println(c1 == c2); 7 } 8 }/* Output: 9 true 10 *///:~ 在代碼的第4行和第5行,,我們分別定義了一個(gè)接受String類型的List和一個(gè)接受Integer類型的List,,按照我們正常的理解,泛型ArrayList<T>雖然是相同的,,但是我們給它傳了不同的類型參數(shù),,那么c1和2的類型應(yīng)該是不同的。但是結(jié)果恰恰想法,,運(yùn)行程序發(fā)現(xiàn)二者的類型時(shí)相同的,。這是為什么呢?這里就要說到Java語言實(shí)現(xiàn)泛型所獨(dú)有的——擦除(萬惡?。?/p> 即當(dāng)我們聲明List<String>和List<Integer>時(shí),,在運(yùn)行時(shí)實(shí)際上是相同的,都是List,,而具體的類型參數(shù)信息String和Integer被擦除了,。這就導(dǎo)致一個(gè)很麻煩的問題:在泛型代碼內(nèi)部,無法獲得任何有關(guān)泛型參數(shù)類型的信息 (摘自《Java編程思想第4版》),。 為了體驗(yàn)萬惡的擦除的“萬惡”,,我們與C++做一個(gè)比較: C++模板: 1 #include <iostream> 2 using namespace std; 3 template<class T> class Manipulator { 4 T obj; 5 public: 6 Manipulator(T x) { obj = x; } 7 void manipulate() { obj.f(); } 8 }; 9 class HasF { 10 public: 11 void f() { cout << "HasF::f()" << endl; } 12 }; 13 int main() { 14 HasF hf; 15 Manipulator<HasF> manipulator(hf); 16 manipulator.manipulate(); 17 } /* Output: 18 HasF::f() 19 ///:~ 在這段代碼中,,我們聲明了一個(gè)模板(即泛型)類Manipulator,這個(gè)類接收一個(gè)T類型的對象,,并在內(nèi)部調(diào)用該對象的f方法,在main我們向Manipulator傳入一個(gè)擁有f方法的類HasF,,然后代碼很正常的通過編譯而且順利運(yùn)行,。 C++代碼里其實(shí)有一個(gè)很奇怪的地方,就是在代碼第7行,,我們利用傳入的T類型對象來調(diào)用它的f方法,,那么我怎么知道你傳入的類型參數(shù)T類型是否有方法f呢?但是從整個(gè)編譯來看,,C++中確實(shí)實(shí)現(xiàn)了,,并且保證了整個(gè)代碼的正確性(可以驗(yàn)證一個(gè)沒有方法f的類傳入,就會報(bào)錯),。至于怎么做到,,我們稍后會略微提及。 OK,,我們將這段代碼用Java實(shí)現(xiàn)下: Java泛型: 1 public class HasF { 2 public void f() { System.out.println("HasF.f()"); } 3 } 4 class Manipulator<T> { 5 private T obj; 6 public Manipulator(T x) { obj = x; } 7 // Error: cannot find symbol: method f(): 8 public void manipulate() { obj.f(); } 9 } 10 public class Manipulation { 11 public static void main(String[] args) { 12 HasF hf = new HasF(); 13 Manipulator<HasF> manipulator = 14 new Manipulator<HasF>(hf); 15 manipulator.manipulate(); 16 } 17 } ///:~ 大家會發(fā)現(xiàn)在C++我們很方便就能實(shí)現(xiàn)的效果,,在Java里無法辦到,在代碼第7行給出了錯誤提示,,就是說在Manipulator內(nèi)部我們無法獲知類型T是否含有方法f,。這是為什么呢?就是因?yàn)槿f惡的擦除引起的,,在Java代碼運(yùn)行的時(shí)候,,它會將泛型類的類型信息T擦除掉,就是說運(yùn)行階段,,泛型類代碼內(nèi)部完全不知道類型參數(shù)的任何信息,。如上面代碼,運(yùn)行階段Manipulator<HasF>類的類型信息會被擦除,,只剩下Mainipulator,,所以我們在Manipulator內(nèi)部并不知道傳入的參數(shù)類型時(shí)HasF的,所以第8行代碼obj調(diào)用f自然就會報(bào)錯(就是我哪知道你有沒有f方法?。?/p> 綜上,,我們可以看出擦除帶來的代價(jià):在泛型類或者說泛型方法內(nèi)部,我們無法獲得任何類型信息,,所以泛型不能用于顯示的引用運(yùn)行時(shí)類型的操作之中,,例如轉(zhuǎn)型、instanceof操作和new表達(dá)式,。例如下代碼: 1 public class Animal<T>{ 2 T a; 3 public Animal(T a){ 4 this.a = a; 5 } 6 // error! 7 public void animalMove(){ 8 a.move(); 9 } 10 // error! 11 public void animalBark(){ 12 a.bark(); 13 } 14 // error! 15 public void animalNew(){ 16 return new T(); 17 } 18 // error! 19 public boolean isDog(){ 20 return T instanceof Dog; 21 } 22 } 23 public class Dog{ 24 public void move(){ 25 System.out.println("dog move"); 26 } 27 public void bark(){ 28 System.out.println("wang!wang!); 29 } 30 } 31 public static void main(String[] args){ 32 Animal<Dog> ad = new Animal<Dog>(); 33 } 我們聲明一個(gè)泛化的Animal類,,之后聲明一個(gè)Dog類,,Dog類可以移動move(),吠叫bark(),。在main中將Dog作為類型參數(shù)傳遞給Animal<Dog>,。而在代碼的第8行和第11行,我們嘗試調(diào)用傳入類的函數(shù)move()和bark(),,發(fā)現(xiàn)會有錯誤,;在代碼16行,我們試圖返回一個(gè)T類型的對象即new一個(gè),,也會得到錯誤,;而在代碼20行,當(dāng)我們試圖利用instanceof判斷T是否為Dog類型時(shí),,同樣是錯誤,! 另外,我這里想強(qiáng)調(diào)下Java泛型是不支持基本類型的(基本類型可參見【Java心得總結(jié)一】Java基本類型和包裝類型解析)感謝CCQLegend 所以還是上面我們說過的話:在泛型代碼內(nèi)部,,無法獲得任何有關(guān)泛型參數(shù)類型的信息 (摘自《Java編程思想第4版》),,我們在編寫泛化類的時(shí)候,我們要時(shí)刻提醒自己,,我們傳入的參數(shù)T僅僅是一個(gè)Object類型,,任何具體類型信息我們都是未知的。 二,、為什么Java用擦除 上面我們簡單闡述了Java中泛型的一個(gè)擦除問題,,也體會到它的萬惡,給我們編程帶來的不便,。那Java開發(fā)者為什么要這么干呢,? 這是一個(gè)歷史問題,Java在版本1.0中是不支持泛型的,,這就導(dǎo)致了很大一批原有類庫是在不支持泛型的Java版本上創(chuàng)建的,。而到后來Java逐漸加入了泛型,為了使得原有的非泛化類庫能夠在泛化的客戶端使用,,Java開發(fā)者使用了擦除進(jìn)行了折中,。 所以Java使用這么具有局限性的泛型實(shí)現(xiàn)方法就是從非泛化代碼到泛化代碼的一個(gè)過渡,以及不破壞原有類庫的情況下,,將泛型融入Java語言,。 三、怎么解決擦除帶來的煩惱 解決方案1: 不要使用Java語言,。這是廢話,,但是確實(shí),當(dāng)你使用python和C++等語言,,你會發(fā)現(xiàn)在這兩種語言中使用泛型是一件非常輕松加隨意的事情,,而在Java中是事情要變得復(fù)雜得多,。如下示例: python: 1 class Dog: 2 def speak(self): 3 print "Arf!" 4 def sit(self): 5 print "Sitting" 6 def reproduce(self): 7 pass 8 9 class Robot: 10 def speak(self): 11 print "Click!" 12 def sit(self): 13 print "Clank!" 14 def oilChange(self) : 15 pass 16 17 def perform(anything): 18 anything.speak() 19 anything.sit() 20 21 a = Dog() 22 b = Robot() 23 perform(a) 24 perform(b) python的泛型使用簡直稱得上寫意,定義兩個(gè)類:Dog和Robot,,然后直接用anything來聲明一個(gè)perform泛型方法,,在這個(gè)泛型方法中我們分別調(diào)用了anything的speak()和sit()方法。 C++ 1 class Dog { 2 public: 3 void speak() {} 4 void sit() {} 5 void reproduce() {} 6 }; 7 8 class Robot { 9 public: 10 void speak() {} 11 void sit() {} 12 void oilChange() { 13 }; 14 15 template<class T> void perform(T anything) { 16 anything.speak(); 17 anything.sit(); 18 } 19 20 int main() { 21 Dog d; 22 Robot r; 23 perform(d); 24 perform(r); 25 } ///:~ C++中的聲明相對來說條條框框多一點(diǎn),,但是同樣能夠?qū)崿F(xiàn)我們要達(dá)到的目的 Java: 1 public interface Performs { 2 void speak(); 3 void sit(); 4 } ///:~ 5 class PerformingDog extends Dog implements Performs { 6 public void speak() { print("Woof!"); } 7 public void sit() { print("Sitting"); } 8 public void reproduce() {} 9 } 10 class Robot implements Performs { 11 public void speak() { print("Click!"); } 12 public void sit() { print("Clank!"); } 13 public void oilChange() {} 14 } 15 class Communicate { 16 public static <T extends Performs> void perform(T performer) { 17 performer.speak(); 18 performer.sit(); 19 } 20 } 21 public class DogsAndRobots { 22 public static void main(String[] args) { 23 PerformingDog d = new PerformingDog(); 24 Robot r = new Robot(); 25 Communicate.perform(d); 26 Communicate.perform(r); 27 } 28 } Java代碼很奇怪的用到了一個(gè)接口Perform,,然后在代碼16行定義泛型方法的時(shí)候指明了<T extends Perform>(泛型方法的聲明方式請見:【Java心得總結(jié)三】Java泛型上——初識泛型),聲明泛型的時(shí)候我們不是簡單的直接<T>而是確定了一個(gè)邊界,,相當(dāng)于告訴編譯器:傳入的這個(gè)類型一定是繼承自Perform接口的,那么T就一定有speak()和sit()這兩個(gè)方法,,你就放心的調(diào)用吧,。 可以看出Java的泛型使用方式很繁瑣,程序員需要考慮很多事情,,不能夠按照正常的思維方式去處理,。因?yàn)檎N覀兪沁@么想的:我定義一個(gè)接收任何類型的方法,然后在這個(gè)方法中調(diào)用傳入類型的一些方法,,而你有沒有這個(gè)方法,,那是編譯器要做的事情。 其實(shí)在python和C++中也是有這個(gè)接口的,,只不過它是隱式的,,程序員不需要自己去實(shí)現(xiàn),編譯器會自動處理這個(gè)情況,。 解決方案2: 當(dāng)然啦,,很多情況下我們還是要使用Java中的泛型的,怎么解決這個(gè)頭疼的問題呢,?顯示的傳遞類型的Class對象: 從上面的分析我們可以看出Java的泛型類或者泛型方法中,,對于傳入的類型參數(shù)的類型信息是完全丟失的,是被擦除掉的,,我們在里面連個(gè)new都辦不到,,這時(shí)候我們就可以利用Java的RTTI即運(yùn)行時(shí)類型信息(后續(xù)博文)來解決,如下: 1 class Building {} 2 class House extends Building {} 3 public class ClassTypeCapture<T> { 4 Class<T> kind; 5 T t; 6 public ClassTypeCapture(Class<T> kind) { 7 this.kind = kind; 8 } 9 public boolean f(Object arg) { 10 return kind.isInstance(arg); 11 } 12 public void newT(){ 13 t = kind.newInstance(); 14 } 15 public static void main(String[] args) { 16 ClassTypeCapture<Building> ctt1 = 17 new ClassTypeCapture<Building>(Building.class); 18 System.out.println(ctt1.f(new Building())); 19 System.out.println(ctt1.f(new House())); 20 ClassTypeCapture<House> ctt2 = 21 new ClassTypeCapture<House>(House.class); 22 System.out.println(ctt2.f(new Building())); 23 System.out.println(ctt2.f(new House())); 24 } 25 }/* Output: 26 true 27 false 28 true 29 *///:~ 在前面的例子中我們利用instanceof來判斷類型失敗,,因?yàn)榉盒椭蓄愋托畔⒁呀?jīng)被擦除了,,代碼第10行這里我們使用動態(tài)的isInstance(),并且傳入類型標(biāo)簽Class<T>這樣的話我們只要在聲明泛型類時(shí),,利用構(gòu)造函數(shù)將它的Class類型信息傳入到泛化類中,,這樣就補(bǔ)償擦除問題 而在代碼第13行這里我們同樣可利用工廠對象Class對象來通過newInstance()方法得到一個(gè)T類型的實(shí)例。(這在C++中完全可以利用t = new T();實(shí)現(xiàn),,但是Java中丟失了類型信息,,我無法知道T類型是否擁有無參構(gòu)造函數(shù)) (上面提到的Class,、isInstance(),newInstance()等Java中類型信息的相關(guān)后續(xù)博文中我自己再總結(jié)) 解決方案3: 在解決方案1中我們提到了,利用邊界來解決Java對泛型的類型擦除問題,。就是我們聲明一個(gè)接口,,然后在聲明泛化類或者泛化方法的時(shí)候,顯示的告訴編譯器<T extends Interface>其中Interface是我們?nèi)我饴暶鞯囊粋€(gè)接口,,這樣在內(nèi)部我們就能夠知道T擁有哪些方法和T的部分類型信息,。 四、通配符之協(xié)變,、逆變 在使用Java中的容器的時(shí)候,,我們經(jīng)常會遇到類似List<? extends Fruit>這種聲明,這里問號?就是通配符,。Fruit是一個(gè)水果類型基類,,它的導(dǎo)出類型有Apple、Orange等等,。 協(xié)變: 1 class Fruit {} 2 class Apple extends Fruit {} 3 class Jonathan extends Apple {} 4 class Orange extends Fruit {} 5 public class CovariantArrays { 6 public static void main(String[] args) { 7 Fruit[] fruit = new Apple[10]; 8 fruit[0] = new Apple(); // OK 9 fruit[1] = new Jonathan(); // OK 10 // Runtime type is Apple[], not Fruit[] or Orange[]: 11 try { 12 // Compiler allows you to add Fruit: 13 fruit[0] = new Fruit(); // ArrayStoreException 14 } catch(Exception e) { System.out.println(e); } 15 try { 16 // Compiler allows you to add Oranges: 17 fruit[0] = new Orange(); // ArrayStoreException 18 } catch(Exception e) { System.out.println(e); } 19 } 20 } /* Output: 21 java.lang.ArrayStoreException: Fruit 22 java.lang.ArrayStoreException: Orange 23 *///:~ 首先我們觀察一下數(shù)組當(dāng)中的協(xié)變(協(xié)變就是子類型可以被當(dāng)作基類型使用),,Java數(shù)組是支持協(xié)變的。如上述代碼,,我們會發(fā)現(xiàn)聲明的一個(gè)Apple數(shù)組用Fruit引用來存儲,,但是當(dāng)我們往里添加元素的時(shí)候我們只能添加Apple對象及其子類型的對象,如果試圖添加別的Fruit的子類型如Orange,,那么在編譯器就會報(bào)錯,,這是非常合理的,一個(gè)Apple類型的數(shù)組很明顯不能放Orange進(jìn)去,;但是在代碼13行我們會發(fā)現(xiàn),,如果想要將Fruit基類型的對象放入,編譯器是允許的,,因?yàn)槲覀兊臄?shù)組引用是Fruit類型的,,但是在運(yùn)行時(shí)編譯器會發(fā)現(xiàn)實(shí)際上Fruit引用處理的是一個(gè)Apple數(shù)組,這是就會拋出異常,。 然而我們把數(shù)組的這個(gè)操作翻譯到List上去,,如下: 1 public class GenericsAndCovariance { 2 public static void main(String[] args) { 3 // Wildcards allow covariance: 4 List<? extends Fruit> flist = new ArrayList<Apple>(); 5 // Compile Error: can’t add any type of object: 6 // flist.add(new Apple()); 7 // flist.add(new Fruit()); 8 // flist.add(new Object()); 9 flist.add(null); // Legal but uninteresting 10 // We know that it returns at least Fruit: 11 Fruit f = flist.get(0); 12 } 13 } ///:~ 我們這里使用了通配符<? extends Fruit>,可以理解為:具有任何從Fruit繼承的類型的列表,。我們會發(fā)現(xiàn)不僅僅是Orange對象不允許放入List,,這時(shí)候極端的連Apple都不允許我們放入這個(gè)List中。這說明了一個(gè)問題List是不能像數(shù)組那樣擁有協(xié)變性,。 這里為什么會出現(xiàn)這樣的情況,,通過查看ArrayList的源碼我們會發(fā)現(xiàn):當(dāng)我們聲明ArrayList<? extends Fruit>中的add()的參數(shù)也變成了"? extends Fruit",這時(shí)候編譯器無法知道你具體要添加的是Fruit的哪個(gè)具體子類型,,那么它就會不接受任何類型的Fruit,。 但是這里我們發(fā)現(xiàn)我們能夠正常的get()出一個(gè)元素的,,很好理解,因?yàn)槲覀兟暶鞯念愋蛥?shù)是<? extends Fruit>,,編譯器肯定可以安全的將元素返回,,應(yīng)為我知道放在List中的一定是一個(gè)Fruit,那么返回就好,。 逆變: 上面我們發(fā)現(xiàn)get方法是可以的,,那么當(dāng)我們想用set方法或者add方法的時(shí)候怎么辦?就可以使用逆變即超類型通配符,。如下: 1 public class SuperTypeWildcards { 2 static void writeTo(List<? super Apple> apples) { 3 apples.add(new Apple()); 4 apples.add(new Jonathan()); 5 // apples.add(new Fruit()); // Error 6 } 7 } ///:~ 這里<? super Apple>意即這個(gè)List存放的是Apple的某種基類型,,那么我將Apple或其子類型放入到這個(gè)List中肯定是安全的。
總結(jié)一下: <? super T>逆變指明泛型類持有T的基類,,則T肯定可以放入 <? extends T>指明泛型類持有T的導(dǎo)出類,,則返回值一定可作為T的協(xié)變類型返回
說了這么多,總結(jié)了一堆也發(fā)現(xiàn)了Java泛型真的很渣,,不好用,對程序員的要求會更高一些,,一不小心就會出錯,。這也就是我們使用類庫中的泛化類時(shí)常看到各種各樣的警告的原因了,。,。。
參考——《Java編程思想第4版》 上面在通配符這里本人理解還不是很透徹,,以后我也會根據(jù)自己理解修改整理,。 |
|