一、萬惡的擦除
我在自己總結的【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語言實現泛型所獨有的——擦除(萬惡啊)
即當我們聲明List<String>和List<Integer>時,在運行時實際上是相同的,都是List,而具體的類型參數信息String和Integer被擦除了。這就導致一個很麻煩的問題:在泛型代碼內部,無法獲得任何有關泛型參數類型的信息 (摘自《Java編程思想第4版》)。
為了體驗萬惡的擦除的“萬惡”,我們與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實現下:
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類型,任何具體類型信息我們都是未知的。
二、為什么Java用擦除
上面我們簡單闡述了Java中泛型的一個擦除問題,也體會到它的萬惡,給我們編程帶來的不便。那Java開發者為什么要這么干呢?
這是一個歷史問題,Java在版本1.0中是不支持泛型的,這就導致了很大一批原有類庫是在不支持泛型的Java版本上創建的。而到后來Java逐漸加入了泛型,為了使得原有的非泛化類庫能夠在泛化的客戶端使用,Java開發者使用了擦除進行了折中。
所以Java使用這么具有局限性的泛型實現方法就是從非泛化代碼到泛化代碼的一個過渡,以及不破壞原有類庫的情況下,將泛型融入Java語言。
三、怎么解決擦除帶來的煩惱
解決方案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的部分類型信息。
四、通配符之協變、逆變
在使用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版》
上面在通配符這里本人理解還不是很透徹,以后我也會根據自己理解修改整理。