1、萬惡的擦除html
我在本身總結的【Java心得總結三】Java泛型上——初識泛型這篇博文中提到了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行,咱們分別定義了一個接受String類型的List和一個接受Integer類型的List,按照咱們正常的理解,泛型ArrayList<T>雖然是相同的,可是咱們給它傳了不一樣的類型參數,那麼c1和2的類型應該是不一樣的。可是結果偏偏想法,運行程序發現兩者的類型時相同的。這是爲何呢?這裏就要說到Java語言實現泛型所獨有的——擦除(萬惡啊)python
即當咱們聲明List<String>和List<Integer>時,在運行時其實是相同的,都是List,而具體的類型參數信息String和Integer被擦除了。這就致使一個很麻煩的問題:在泛型代碼內部,沒法得到任何有關泛型參數類型的信息 (摘自《Java編程思想第4版》)。ios
爲了體驗萬惡的擦除的「萬惡」,咱們與C++作一個比較:程序員
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 ///:~
在這段代碼中,咱們聲明瞭一個模板(即泛型)類Manipulator,這個類接收一個T類型的對象,並在內部調用該對象的f方法,在main咱們向Manipulator傳入一個擁有f方法的類HasF,而後代碼很正常的經過編譯並且順利運行。數組
C++代碼裏其實有一個很奇怪的地方,就是在代碼第7行,咱們利用傳入的T類型對象來調用它的f方法,那麼我怎麼知道你傳入的類型參數T類型是否有方法f呢?可是從整個編譯來看,C++中確實實現了,而且保證了整個代碼的正確性(能夠驗證一個沒有方法f的類傳入,就會報錯)。至於怎麼作到,咱們稍後會略微說起。安全
OK,咱們將這段代碼用Java實現下:app
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 } ///:~
你們會發如今C++咱們很方便就能實現的效果,在Java裏沒法辦到,在代碼第7行給出了錯誤提示,就是說在Manipulator內部咱們沒法獲知類型T是否含有方法f。這是爲何呢?就是由於萬惡的擦除引發的,在Java代碼運行的時候,它會將泛型類的類型信息T擦除掉,就是說運行階段,泛型類代碼內部徹底不知道類型參數的任何信息。如上面代碼,運行階段Manipulator<HasF>類的類型信息會被擦除,只剩下Mainipulator,因此咱們在Manipulator內部並不知道傳入的參數類型時HasF的,因此第8行代碼obj調用f天然就會報錯(就是我哪知道你有沒有f方法啊)
綜上,咱們能夠看出擦除帶來的代價:在泛型類或者說泛型方法內部,咱們沒法得到任何類型信息,因此泛型不能用於顯示的引用運行時類型的操做之中,例如轉型、instanceof操做和new表達式。例以下代碼:
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 }
咱們聲明一個泛化的Animal類,以後聲明一個Dog類,Dog類能夠移動move(),吠叫bark()。在main中將Dog做爲類型參數傳遞給Animal<Dog>。而在代碼的第8行和第11行,咱們嘗試調用傳入類的函數move()和bark(),發現會有錯誤;在代碼16行,咱們試圖返回一個T類型的對象即new一個,也會獲得錯誤;而在代碼20行,當咱們試圖利用instanceof判斷T是否爲Dog類型時,一樣是錯誤!
另外,我這裏想強調下Java泛型是不支持基本類型的(基本類型可參見【Java心得總結一】Java基本類型和包裝類型解析)感謝CCQLegend
因此仍是上面咱們說過的話:在泛型代碼內部,沒法得到任何有關泛型參數類型的信息 (摘自《Java編程思想第4版》),咱們在編寫泛化類的時候,咱們要時刻提醒本身,咱們傳入的參數T僅僅是一個Object類型,任何具體類型信息咱們都是未知的。
2、爲何Java用擦除
上面咱們簡單闡述了Java中泛型的一個擦除問題,也體會到它的萬惡,給咱們編程帶來的不便。那Java開發者爲何要這麼幹呢?
這是一個歷史問題,Java在版本1.0中是不支持泛型的,這就致使了很大一批原有類庫是在不支持泛型的Java版本上建立的。而到後來Java逐漸加入了泛型,爲了使得原有的非泛化類庫可以在泛化的客戶端使用,Java開發者使用了擦除進行了折中。
因此Java使用這麼具備侷限性的泛型實現方法就是從非泛化代碼到泛化代碼的一個過渡,以及不破壞原有類庫的狀況下,將泛型融入Java語言。
3、怎麼解決擦除帶來的煩惱
解決方案1:
不要使用Java語言。這是廢話,可是確實,當你使用python和C++等語言,你會發如今這兩種語言中使用泛型是一件很是輕鬆加隨意的事情,而在Java中是事情要變得複雜得多。以下示例:
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的泛型使用簡直稱得上寫意,定義兩個類:Dog和Robot,而後直接用anything來聲明一個perform泛型方法,在這個泛型方法中咱們分別調用了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++中的聲明相對來講條條框框多一點,可是一樣可以實現咱們要達到的目的
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代碼很奇怪的用到了一個接口Perform,而後在代碼16行定義泛型方法的時候指明瞭<T extends Perform>(泛型方法的聲明方式請見:【Java心得總結三】Java泛型上——初識泛型),聲明泛型的時候咱們不是簡單的直接<T>而是肯定了一個邊界,至關於告訴編譯器:傳入的這個類型必定是繼承自Perform接口的,那麼T就必定有speak()和sit()這兩個方法,你就放心的調用吧。
能夠看出Java的泛型使用方式很繁瑣,程序員須要考慮不少事情,不可以按照正常的思惟方式去處理。由於正常咱們是這麼想的:我定義一個接收任何類型的方法,而後在這個方法中調用傳入類型的一些方法,而你有沒有這個方法,那是編譯器要作的事情。
其實在python和C++中也是有這個接口的,只不過它是隱式的,程序員不須要本身去實現,編譯器會自動處理這個狀況。
解決方案2:
固然啦,不少狀況下咱們仍是要使用Java中的泛型的,怎麼解決這個頭疼的問題呢?顯示的傳遞類型的Class對象:
從上面的分析咱們能夠看出Java的泛型類或者泛型方法中,對於傳入的類型參數的類型信息是徹底丟失的,是被擦除掉的,咱們在裏面連個new都辦不到,這時候咱們就能夠利用Java的RTTI即運行時類型信息(後續博文)來解決,以下:
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來判斷類型失敗,由於泛型中類型信息已經被擦除了,代碼第10行這裏咱們使用動態的isInstance(),而且傳入類型標籤Class<T>這樣的話咱們只要在聲明泛型類時,利用構造函數將它的Class類型信息傳入到泛化類中,這樣就補償擦除問題
而在代碼第13行這裏咱們一樣可利用工廠對象Class對象來經過newInstance()方法獲得一個T類型的實例。(這在C++中徹底能夠利用t = new T();實現,可是Java中丟失了類型信息,我沒法知道T類型是否擁有無參構造函數)
(上面提到的Class、isInstance(),newInstance()等Java中類型信息的相關後續博文中我本身再總結)
解決方案3:
在解決方案1中咱們提到了,利用邊界來解決Java對泛型的類型擦除問題。就是咱們聲明一個接口,而後在聲明泛化類或者泛化方法的時候,顯示的告訴編譯器<T extends Interface>其中Interface是咱們任意聲明的一個接口,這樣在內部咱們就可以知道T擁有哪些方法和T的部分類型信息。
4、通配符之協變、逆變
在使用Java中的容器的時候,咱們常常會遇到相似List<? extends Fruit>這種聲明,這裏問號?就是通配符。Fruit是一個水果類型基類,它的導出類型有Apple、Orange等等。
協變:
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 *///:~
首先咱們觀察一下數組當中的協變(協變就是子類型能夠被看成基類型使用),Java數組是支持協變的。如上述代碼,咱們會發現聲明的一個Apple數組用Fruit引用來存儲,可是當咱們往裏添加元素的時候咱們只能添加Apple對象及其子類型的對象,若是試圖添加別的Fruit的子類型如Orange,那麼在編譯器就會報錯,這是很是合理的,一個Apple類型的數組很明顯不能放Orange進去;可是在代碼13行咱們會發現,若是想要將Fruit基類型的對象放入,編譯器是容許的,由於咱們的數組引用是Fruit類型的,可是在運行時編譯器會發現實際上Fruit引用處理的是一個Apple數組,這是就會拋出異常。
然而咱們把數組的這個操做翻譯到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繼承的類型的列表。咱們會發現不只僅是Orange對象不容許放入List,這時候極端的連Apple都不容許咱們放入這個List中。這說明了一個問題List是不能像數組那樣擁有協變性。
這裏爲何會出現這樣的狀況,經過查看ArrayList的源碼咱們會發現:當咱們聲明ArrayList<? extends Fruit>中的add()的參數也變成了"? extends Fruit",這時候編譯器沒法知道你具體要添加的是Fruit的哪一個具體子類型,那麼它就會不接受任何類型的Fruit。
可是這裏咱們發現咱們可以正常的get()出一個元素的,很好理解,由於咱們聲明的類型參數是<? extends Fruit>,編譯器確定能夠安全的將元素返回,應爲我知道放在List中的必定是一個Fruit,那麼返回就好。
逆變:
上面咱們發現get方法是能夠的,那麼當咱們想用set方法或者add方法的時候怎麼辦?就可使用逆變即超類型通配符。以下:
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>意即這個List存放的是Apple的某種基類型,那麼我將Apple或其子類型放入到這個List中確定是安全的。
總結一下:
<? super T>逆變指明泛型類持有T的基類,則T確定能夠放入
<? extends T>指明泛型類持有T的導出類,則返回值必定可做爲T的協變類型返回
說了這麼多,總結了一堆也發現了Java泛型真的很渣,很差用,對程序員的要求會更高一些,一不當心就會出錯。這也就是咱們使用類庫中的泛化類時常看到各類各樣的警告的緣由了。。。
參考——《Java編程思想第4版》
上面在通配符這裏本人理解還不是很透徹,之後我也會根據本身理解修改整理。