久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

【Java心得總結(jié)四】Java泛型下——萬惡的擦除

 昵稱45388030 2017-07-13

原文網(wǎng)址:http://www.cnblogs.com/xltcjylove/p/3671943.html


一,、萬惡的擦除

我在自己總結(jié)的【Java心得總結(jié)三】Java泛型上——初識泛型這篇博文中提到了Java中對泛型擦除的問題,考慮下面代碼:

復(fù)制代碼
 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 *///:~ 
復(fù)制代碼

在代碼的第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++模板:

復(fù)制代碼
 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 ///:~ 
復(fù)制代碼

在這段代碼中,,我們聲明了一個(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泛型:

復(fù)制代碼
 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ù)制代碼

大家會發(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á)式,。例如下代碼:

復(fù)制代碼
 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 }
復(fù)制代碼

我們聲明一個(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:

復(fù)制代碼
 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)
復(fù)制代碼

python的泛型使用簡直稱得上寫意,定義兩個(gè)類:Dog和Robot,,然后直接用anything來聲明一個(gè)perform泛型方法,,在這個(gè)泛型方法中我們分別調(diào)用了anything的speak()和sit()方法。

C++

復(fù)制代碼
 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 } ///:~ 
復(fù)制代碼

C++中的聲明相對來說條條框框多一點(diǎn),,但是同樣能夠?qū)崿F(xiàn)我們要達(dá)到的目的

Java:

復(fù)制代碼
 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 }
復(fù)制代碼

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ù)博文)來解決,如下:

復(fù)制代碼
 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 *///:~ 
復(fù)制代碼

在前面的例子中我們利用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é)變:

復(fù)制代碼
 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 *///:~
復(fù)制代碼

首先我們觀察一下數(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上去,,如下:

復(fù)制代碼
 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 } ///:~ 
復(fù)制代碼

我們這里使用了通配符<? 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í)候怎么辦?就可以使用逆變即超類型通配符,。如下:

復(fù)制代碼
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 } ///:~ 
復(fù)制代碼

這里<? 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ù)自己理解修改整理,。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多