Java:泛型的理解

本文源自參考《Think in Java》,多篇博文以及閱讀源碼的總結java

前言

Java中的泛型每各人都在使用,可是它底層的實現方法是什麼呢,爲什麼要這樣實現,這樣實現的優缺點有哪些,怎麼解決泛型帶來的問題。帶着好奇,我查閱資料進行了初步的學習,在此與諸位探討。編程

一 類型參數

學過JAVA的人都知道泛型,明白大概怎麼使用。在類上爲:class 類名 {},在方法上爲:public void 方法名 (T x){}。泛型的實現使得類型變成了參數能夠傳入,使得類功能多樣化。 安全

具體可分爲5種狀況:編程語言

  1. T是成員變量的類型
  2. T是泛型變量(不管成員變量仍是局部變量)的類型參數,常見如Class<T>,List<T>
  3. T是方法拋出的Exception(要求<T extends Exception>
  4. T是方法的返回值
  5. T是方法的參數

1.1 泛型的實現

JAVA的泛型是基於編譯器實現的,使用了擦除的方法實現,這是由於java1.5以後纔出現了泛型,爲了保持向後兼容而作出的妥協。學習

所謂擦除就是JAVA文件在編譯成字節碼時類型參數會被擦除掉,單獨記錄在其餘地方。而且用類型參數的父類代替原有的位置。
假設參數類型的佔位符爲T,擦除規則以下:code

  1. <T>擦除後變爲Obecjt
  2. <? extends A>擦除後變爲A
  3. <? super A>擦除後變爲Object

這種規則叫作保留上界對象

編譯器擦除類型參數後,經過JAVA的強制轉換保證了類型參數在使用時的正確。如:在類型參數T中傳入了類A,那麼編譯器會在全部類A將返回(拋出)類型參數T的代碼處加上(A)進行強轉.blog

舉個栗子:get

ArrayList<String> list = new ArrayList<String>();
        list.add("123");
        String b = list.get(0);

在編譯後會變成編譯器

ArrayList list = new ArrayList();//沒有參數即默認爲Object
        list.add("123");
        String b = (String) list.get(0);

而且會在帶有類型參數類的子類中造成橋方法保證了多態性。
具體參考官方解釋以下

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

二 通配符?

在帶有類型參數的類內部,代碼仍然按照參數類型擦除後的父類來處理。可是擦除存在一個問題,在這種機制下泛型是不變的,而沒有逆變和協變。

2.1 逆變與協變


協變和逆變網上有不少解釋,顯得模糊不清,我參考幾個編程語言的官方解釋後給出一個比較寬泛的定義。協變指可以使用比原始聲明類型的派生程度更大(更具體的)的類型,逆變指可以使用比原始聲明類型的派生程度更小(不太具體的)的類型。
如:
Object obj = new String("123");
這就是協變,將String這個更具體的(子類)類型賦給了本來較寬泛定義(父類)的類型Object。
JAVA不容許將父類賦給子類,天然Java不支持逆變。

網上不少博文說JAVA泛型也有逆變,我是不贊同的,那只是一種模擬的逆變,即有部分逆變的特性並且看起來像逆變,具體分析後文會給出


2.2 Java中的逆變與協變

在JAVA中,

List<Integer> b = new ArrayList<Integer>()
List<NumFber> a = b;

是沒法經過編譯器檢查的。不容許這樣作有一個很充分的理由:這樣作將破壞要泛型的類型安全。若是可以將List<Integer> 賦給List<Number>。那麼下面的代碼就容許將非Integer的內容放入 List<Integer>

List<Integer> b = new ArrayList<Integer>(); 
List<Number> a = b; // illegal 
a.add(new Float(3.1415));

由於aList<Number>,因此向其添加Float彷佛是徹底可行的。可是若是a實際是List<Integer>,那麼這就破壞了蘊含在b中定義的類型聲明 —— 它是一個整數列表,這就是泛型類型不能協變的緣由。但也所以使得泛型失去了多態的拓展性。

2.3 通配符解決協變

Java官方經過加入了通配符?來解決泛型協變的問題。這樣就能經過編譯了:

List<Integer> b = new ArrayList<Integer>(); 
List<? extends Number> a = b;

能夠解讀爲a是一種帶有NumberList集合類,在從a中取出數據的時候統一當作Number處理就好了。同時這也是符合里氏替換原則的

可是編譯器會禁止你將將類Integer放入a,即a.add(new Integer(1))//illegal
這也很合理,由於你聲明的a原本就沒有限定a包含的具體是哪一個Number子類,所以不許任何變量的添加保證了泛型的安全性。
解決往a添加對象的方法也很簡單

List<Object> b = new ArrayList<Object>(); 
List<? super Number> a = b;

a是某種Number父類的List集合類,將ArrayList<Object>賦給a也是合情合理的,Object確實是Number的父類。這也符合里氏替換原則的

(網上大部分博文說這就是逆變,可是仔細想一想逆變的官方定義,在JAVA中能夠理解爲:類T是類S的子類,而類A<T>是類A<S>的父類。仔細看看List<? super Number>List<Object>的關係,在這裏TNumber,而SObject,可是List<? super Number>從邏輯上來看真的是List<Object>的子類嗎,若是單純從字面上來看List<? super Number>是帶有Number父類的集合類,根據保留上界的擦除方法,應該擦除爲List<Object>,將一個List<Object>賦給另外一個List<Object>是不存在任何逆變的。我在疑惑之下去谷歌查閱了資料,也沒有英文資料說明JAVA泛型裏這屬於逆變)

相關文章
相關標籤/搜索