經過實例來學習java/kotlin泛型

泛型算是一個比較典型的易學難精通的東東. 好比說寫一個棧, 你們都知道要用class Stack<T> {..} 這樣的寫法, 可是一到更復雜的場景, 咱們就常常被IDE報出來的莫名其妙的泛型錯誤給搞暈. 不知道爲何出錯, 也不知道如何修復. 因此這篇文章不會像網上不少其它介紹泛型的文章同樣一個個知識點地介紹. 而是側重於實例, 來說解工做中的泛型難題.java

I. 泛型爲何產生

1. 老版本中, 集合中的get()可能出錯

在java1.0到java1.4的時候裏, 是沒有泛型的. 因此咱們的List等集合能夠聽任意數據. 但list.get(0)取出來時, 就要強轉 String value = (String) list.get(0)數組

這樣的問題就是當咱們不當心地在前面操做過list.add(20)時, 這時的強轉就會出錯.安全

泛型就是想讓咱們在存,取時都是限定了某一類型. 這樣get()時就不用強制轉換, 就少了ClassCastException這樣的crash了bash

2. java全版本中, 數組實際上是"類型不安全"的

什麼叫"類型不安全"? 舉例來講, 當咱們有一個Animal類, 其有Cat, Dog兩個子類.函數

Animal
    /    \
  Cat    Dog
複製代碼

那下面的代碼確定在編譯時不會報錯 (但智能的IDE, 如Intellij IDEA會報警告),編碼

Dog dog = new Dog();

Cat[] cats = new Cat[1];
Animal[] animals = cats;
animals[0] = dog; 
複製代碼

但是, 上面的代碼一運行就會crash: spa

咱們先來看爲何編譯時不報錯. : 由於animal[]自己就是Animal[], 因此放入dog沒問題啊設計

那爲何運行時又有錯呢? : 這時由於animals = cats, 這就讓animals數組本質上是Cat[]數組. 這時再加入Dog對象, 就會報ArraySotreException. 畢竟Java對數組存儲的類型是有強限制的, 一個Cat[]數組裏不能存Dog對象.3d

3. 泛型就是想解決上面兩種問題

1). List變成List<T>, 一開始就限制好讀寫時的類型code

2). 數組這樣的類型不安全, 要是也用於集合類型, 那就容易在編碼時不報錯, 一上線就crash, 是個隱患. List就是想之後List<Animal>裏不能被cats給賦值, 即不能List<Animal> animals = cats

這一點很重要, 也極其容易混淆. 你要是還看不明白這句話, 沒關係, 後面咱們會更詳細講. 讀到到這裏, 只要理會了數組的類型是不安全的, 就是能夠了的 - 泛型就是想改進它.

II. 泛型的實例

1. 實例一

我有一個類, 其中有一個List<Animal>成員. 在構造函數裏, 我可能要去存值, 或取值.

List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
Animal cat = animals.get(0);
複製代碼

上面的代碼都沒錯, 你們也司空見慣了, 彷佛沒什麼特別的. 是的. 但咱們在另外的場景裏, 相似的代碼就有大大的問題了.

2. 實例二: RecyclerView.Adapter

我如今有一個展現寵物的App. 其中一個RecyclerView.Adapter能夠被展現貓咪的CatList頁, 也能夠被展現狗狗的DogList頁給展現. 兩個頁面的UI同樣, 都是一個RecyclerView, 只是數據不同; 所以咱們的Adapter有一個setData()方法.

由於咱們的數據在CatList頁面裏能夠放入List<Cat>, 也能夠在DogList頁面裏放入List<Dog>, 便可以放入一個Animal的List. 因此我就寫成了這樣:public void setData(List<Animal> animals) {...}

可是我在CatList頁面調用setData()時卻出錯了

意識到上面的圖裏, java編譯器給咱們大聲抱怨了"這有一個問題. 我期待一個List<Animal>, 但卻獲得了一個List<Cat>! ". 這裏就要轉回到上面的數組了.

在數組裏, 咱們用下面代碼是沒問題的:

Cat[] cats = new Cat[1];
Animal[] animals = cats;
複製代碼

但在集合中, 或者說有泛型了以後的集合中, 不能這樣用了:

// 這段代碼是錯的
List<Cat> catList = new ArrayList<>();
List<Animal> animalList = new ArrayList<>();
animalList = catList; //ERROR!!!
複製代碼

緣由上面講過了, 就是由於數組這樣的"類型不安全", 可能把問題要到runtime時才暴露, 因此java泛型不打算這樣. java泛型要求更嚴格的類型檢查. 對java來講, List<Cat>List<Animal>根本不是父子關係 (雖然Cat是Animal的子類), 因此不能使用父類型 obj = 子類型object這樣的賦值. (爲了簡便, 後文都將這樣的賦值稱爲"父子賦值")

p.s. 要是上面理解沒問題, 那這一段就算是額外說明. 要是上面還暈着, 這一段就不看也沒事:

這其實就是java泛型的類型擦除.
即無論你是List<A>仍是List<B>, 這些泛型集合的操做都只在編譯時檢查. 一旦到了運行時, 其實都只是被java當成了List, 即沒了A, B的類型了. 
因此叫類型擦除, 被抹除了, 再也沒有泛型的類型消息了. 

類型擦除實際上是java1.5引入泛型後, 爲了讓.class文件仍與舊版本java兼容, 所採用的沒辦法的辦法. 
否則之前版本的java, List就是一個List, 如今java1.5倒是一個List<A>, 那原來的程序可能crash了. 

複製代碼

3. 解決上面Adapter的問題

上面一小節主要是講爲何java向咱們抱怨代碼有問題. 但是回到現實生活中, 咱們但是有deadline的, 這個問題到底要如何解決呢?

個人需求就是setData()能夠傳入List<Cat>, 也能夠傳入List<Dog>. 這時通配符就來幫咱們忙了.

咱們能夠在setData()中, 把參數由List<Animal>改成List<? extends Animals>. 這樣接受的參數就能夠是List<Animal>, 或是List<Animal子類> ! 即:

// 原來的出錯代碼:
public void setData(List<Animal> animals){ }

// 如今改成:
public void setData(List<? extends Animal> animals){ }
複製代碼

這時咱們再賦值就不會出錯了:

III. List<? extends Animal>

如今產品和咱們說, 爲了營利, 咱們頁面接入了廣告商. 如今不論是CatList頁, 仍是DogList頁, 咱們的展現的數據第一個都是廣告商提供的寵物. 因此咱們要小小修改一下代碼

  1. Animal類新有一個子類:
class AdPet extends Animal{... }
複製代碼
  1. Adapter.setData(list)也要確保第一個就是廣告商的寵物.
public void setData(List<? extends Animal> animals){
        animals.add(0, new AdPet()); //ERROR!
        // ....
    }
複製代碼

這裏代碼卻報錯了:

這裏其實就是泛型讓人很暈的地方了. 這明明是一個List<? extends Animal>, 怎麼add一個Animal的子類,卻不行呢? : 還是要回到上面那個很重要的數組是類型不安全的例子上來. 數組在Animal[] animals = cats以後, 仍能animals.add(dog); -- 但它不該該還能add(dog), 這時應該只能add(cat)了.

泛型就是這樣的 當一個List類型是List<? extends Animal>時, 它能夠被父子賦值catList, 或是dogList, setData()裏面根本不知道. 既然我不知道, 那你再add任何Animal子類可能都會出現相似上面ArrayStoreException的錯誤, 因此我乾脆就不讓你add()了!!!

是的, List<? extends Animal>的對象, 不能add, 只能get(). (它get()返回的天然就是Animal, 這個仍是能保證不出錯的). 再寫個小實例, 來加深咱們的印象.

2. List<? extends Animal>的特色:

  • 它能夠被父子賦值. 如: List<? extends Animal> animalList = new ArrayList<Cat>();
  • 它不能add(), 即不能寫入
  • 它能get(), 即能讀取它

IV. List<? super Animal>

泛型的通配符除了extends, 還有一個suepr:

它正好與extends相反, 它能夠add(Animal子類), 但它不能get(). -- 更精確地說是:

  • 它能夠add(animal子類)
  • 它的get(i)只能返回一個Object類型

這裏要說明的是: 當咱們有一個Animal的父類, 叫Being時:

Being
          |
        Animal
      /       \
    Cat       Dog
複製代碼

List<? super Animal>也支持父子賦值, 但這時賦值的右值得是一個List<Animal或其父類>

這就理解了爲什麼Animal a = animals.get(0)會報錯, 由於它返回的多是一個Being對象, 而不是Animal對象!

2. List<? super Animal>的特色:

  • 它能夠被父子賦值. 如: List<? super Animal> animalList = new ArrayList<Being>();
  • 它能add(), 可寫入Animal與Animal的子類
  • 它不能get()並返回一個Animal對象. -- 它返回的只能是一個Object對象

V. 總結

其實還有一種通配符, 就是<?>通配符. 不過這個出現得很少, 並且讓人更暈, 因此這篇文章中就不介紹它了.

1. 那List<Animal>呢?

這個問題好, 說明你在思考了: "List<Animal>能夠寫, 能夠讀, 能夠支持父子賦值嗎?"

實例說明一切,咱們來看一下例子:

List<Animal>能寫能讀, 但就是不支持父子賦值.

2. 大總結

因此當你在使用, 或設計帶泛型的類時, 就能夠這樣選用: (PC-assign就是我簡稱的"parent-child assign", 是我爲了簡便稱呼自用的術語.)

VI. 彩蛋

上面講了總結出來的理論. 咱們如今再用一個JDK裏的源碼來驗證咱們的理論. 這個源碼出自Collections類的copy()方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
        for(int i = 0; i < srcSize; ++i) {
            dest.set(i, src.get(i));
        }       
    }
}
複製代碼

我想通過上面的解釋, 咱們如今就知道爲什麼兩個參數, 一個是super, 另外一個是extends了. : 我要支持父子賦值, 因此得用通配符. 即copy<Animal>(catList, catList2)得行, 因此我要用通配符.

同時又由於dist只要add(), 而src只要get()就好了, 因此咱們就相應地加上了extends與super.

能夠說, 只要你理解了這個copy()的兩個參數爲什麼一個是super, 爲什麼另外一個是extends, 以及爲什麼要用通配符. 那恭喜你, 這篇文章你過關了.

VII. Kotlin中的泛型

泛型其實不少細節點, 像什麼泛型方法, 這些我都沒在介紹. 由於我想在這篇文章裏集中介紹一些很重要但又很讓人頭暈的知識點.

對應上面的東東, kotlin其實就是in, out兩個來代替了extends與super. 其實還是看Collections.copy()的kotlin版本, 咱們就理解了in, out了:

static fun <T> copy(dest: MutableList<in T>, src: List<out T>) {
  ...
  }
複製代碼

VIII. 結尾

其實這篇文章主要是講泛型裏的通配符的知識. 泛型還有一些其它部分, 如泛型方法這些我都沒有講解, 由於我感受通配符最容易讓人暈, 因此我優先講這一塊.

也歡迎你們評論中說說本身碰到的泛型難題. 有表明性的我就解決後再整理下, 再寫一篇泛型難題的文章

相關文章
相關標籤/搜索