從零起步,真正理解集合與泛型

零、前言

List<E>的尖括號是什麼意思?
什麼是泛型?如何去理解它?
泛型有什麼優勢?面試

以上問題能夠在本文中找到答案。算法

1、基礎知識

在全部編程語言中,都有這麼一種東西,叫作「數組」,好比C++中就有數組編程

可是呢,傳統數組最大的缺點就是:它的長度是固定的,若是不知道一個數組須要容納多大的數據量,編寫程序時就會很困難,數組建的太大了,就會浪費內存資源,數組過小了,容量不夠就會報錯。segmentfault

因此,前人發明了各類各樣的大小可變的數組,也就是「集合」,Collection數組

Java集合類型一覽(選自《HeadFirstJava》):安全

Scan - 2020-04-11 17_05_04.jpg

正由於集合沒有固定的大小,而且有着許多很方便的API,所以集合被普遍使用。(今後之後,原始的數組就只出如今大學的C++課堂之中了)數據結構

衆多的集合中,較爲常見的就是ArrayList,咱們就拿它來舉例。dom

假如,須要一個集合來儲存一些字符串,而後遍歷輸出它們:編程語言

public static void main(String[] args) {
    // new對象,初始化
    ArrayList<String> array = new ArrayList<String>();
    
    // 加入元素
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    
    //遍歷輸出
    for (String i: array) {
        System.out.println(i);
    }
}

那麼問題來了,ArrayList<String>中的<String>有什麼做用呢?ide

若是把剛纔的<String>去掉或者刪掉,會發生什麼呢?

  • 把尖括號裏面改爲Double的包裝類,但傳入的仍是String:
public static void main(String[] args) {
    // 此處的集合是Double類型
    ArrayList<Double> array = new ArrayList<Double>();
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    for (String i: array) {
        System.out.println(i);
    }
}

答案是:沒法經過編譯,傳入的類型與規定類型不匹配。

方法 Collection.add(Double)不適用
  (參數不匹配; String沒法轉換爲Double)
  • 把尖括號裏面去掉,但傳入的仍是String:
public static void main(String[] args) {
    // 此處的集合沒有聲明類型
    ArrayList array = new ArrayList();
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    for (String i: array) {
        System.out.println(i);
    }
}

答案是:仍然沒法經過編譯,沒有進行傳入類型的安全檢查

咱們能夠從中看出一些規律:尖括號<>裏面的內容,彷佛是對這個集合的類型作了規定。

那爲何沒有了這個尖括號,仍是不能經過編譯呢?
理論上,沒有條件的約束,應該能夠儲存任何類型的數據啊?

就像這樣:

public static void main(String[] args) {
    // 沒有類型約束
    ArrayList array = new ArrayList();
    // 字符串
    array.add("ABC");
    // 整數型
    array.add(123);
    // 浮點數
    array.add(123.456);
    // 對象
    array.add(new Object());
    
    for (String i: array) {
        System.out.println(i);
    }
}

固然,上面的代碼是錯的,100%不能經過編譯。

假設能編譯,那麼問題又出現了:在不知道某個元素的類型的狀況下,怎麼使用統一的方法去處理它呢?好比Java內置的Print能夠輸出字符串,但怎麼用System.out.println(i)來打印一個對象的內容呢?

因此這種約束條件,最大的好處就是增長了安全性,就比如不一樣口徑的瓶子,只能裝合適的物品,String類型的瓶子只能裝String類型的對象

這就是今天的話題,泛型

2、初識泛型

泛型是什麼?從字面意思解釋:「泛」是普遍的,「型」是一種特定類型,連起來就是「普遍的特定類型的對象」。

說它普遍,不管是什麼對象,是隻要符合規定的類型,集合(ArrayList)就能夠處理它;
說它特定,是由於它必須嚴格符合約定的類型,不然就會被編譯器攔截下來,沒法經過編譯。

若是說集合是各類瓶子,那麼泛型就是對於瓶子的約束。這種約束是爲了安全,若是沒有了泛型的約束,集合就能夠容納任何類型的對象,啥均可以裝進去,就像把綿羊放進老虎的集合中。

那麼,怎麼使用泛型呢?

首先,不一樣於方法的參數,參數是針對於某個方法來講的,而泛型是針對一個類或對象來講的。
再解釋一下就是:把一個參數傳給某個方法,這個方法接收到參數以後,就能夠處理它;把一個泛型傳給某個類,這個類接收到這個泛型以後,就能夠new出一個只能處理這個泛型的對象。

因此泛型參數有類似之處,關鍵點就是在尖括號<>里加上

泛型能夠用在集合中:

  • new一個對象:
new ArrayList<String>
  • new對象並聲明變量:
// 聲明stringList變量是字符串類型的集合
// 而且連接到一個new出來的對象上
ArrayList<String> stringList = new ArrayList<String>;

泛型還能用在方法的參數中:

// 此方法接收的參數是List集合,
// 但必須是字符串類型的集合才能夠
public void printString (List<String> list) {
    ...
}

爲一個集合添加數據:

// 添加: add
// 刪除: remove
// 其餘用法請參考源碼,寫的很清楚

array.add("ABC");

3、深刻泛型——原理

基本的用法說完了,那麼就來看看,泛型的世界裏有什麼規律。

在以前的《從零起步,真正理解Javascript回調函數》中說到:

程序 = 數據結構 + 算法

若是執行一個寫死的程序(好比輸出HelloWorld),那麼不管怎樣運行,結果都是同樣的。這樣的程序是沒有意義的。

那麼,若是想讓某個方法發揮做用,就要讓它是能夠「變化」的,若是把輸出的數據單獨拿出來,做爲函數的參數,而方法不變,那麼就能夠根據不一樣的參數,用一樣的方法輸出不一樣的結果。
這就是函數。

反之,若是待處理的數據不變,而處理數據的方法改變,把方法做爲參數,就有了回調函數。

以上兩種變化都是對於函數的,而泛型是對於來講的。

咱們看一看ArrayList的源碼(部分):

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    
    public E get(int index) {
        Objects.checkIndex(index, this.size);
        return this.elementData(index);
    }

    public E set(int index, E element) {
        Objects.checkIndex(index, this.size);
        E oldValue = this.elementData(index);
        this.elementData[index] = element;
        return oldValue;
    }

}

能夠看到,這個類的許多方法裏面,都有一個E,這個E是就是泛型,它不是一個具體的類型,而是在new對象的時候,傳入什麼泛型,就是用什麼泛型。
在類定義的時候,有:

ArrayList<E>

既然定義的時候是E,下面的方法裏也是E,那麼,在初始化ArrayList的時候,傳入什麼泛型,下面的E就會變化成什麼泛型,好比

ArrayList<String> stringList = new ArrayList<String>;

此時傳入的是String,那麼上面的代碼,等同於發生了以下變化:

// 全部的 E 都被替換爲特定的類型

public class ArrayList<String> extends AbstractList<String> implements List<String>, RandomAccess, Cloneable, Serializable {

    public String get(int index) {
        Objects.checkIndex(index, this.size);
        return this.elementData(index);
    }

    public String set(int index, String element) {
        Objects.checkIndex(index, this.size);
        String oldValue = this.elementData(index);
        this.elementData[index] = element;
        return oldValue;
    }

}

此時,這個被new出來的集合對象,就只能處理字符串類型的數據了。

(有沒有感受像函數重載?泛型就能夠想象成類的重載,並且不用手動寫重載代碼)

知道了泛型的原理,也就很容易明白一個道理:
泛型並不是只能用於數組和集合中,咱們也能夠建立使用泛型的類,只不過因爲Java的API很豐富,沒有必要再去本身寫了。

4、最終篇 集合套娃

已經講了不少知識,接下來來實踐一下,若是能明白這個實例,就真正理解泛型了。

有這麼一道面試題:

// 要求:給變量賦值

List<Map<String,List<String>>> list = newArrayList<Map<String,List<String>>>();

要理解這道題,咱們還須要一些知識的補充。

Java集合類型一覽(選自《HeadFirstJava》):

Scan - 2020-04-11 17_05_04.jpg

集合有這麼多種,可是有些是,有些是接口
衆所衆知,接口是不能New的,因此題目中的List和Map都是不能new的,但咱們能夠new它們的實現類,而後賦值給這個類型的變量。

接着看題,變量名是list,類型是List集合,泛型是<Map<String,List<String>>>

那麼怎麼理解呢?就像洋蔥「同樣一層一層的剝開它的心」。

圖片.png

  • 最外層,是一個List,它的泛型是<Map>
  • 第二層,是一個Map,它的泛型是<String, List>
  • 最裏層,又是一個List,它的泛型是<String>

Map爲何有兩個泛型呢?
請參考函數的兩個參數,Map就至關於兩個參數的函數
因爲Map具備特殊性,它是「鍵值對」,Map的每個「」都必須有一個惟一的「」,因此要寫成 Map<鍵,值>

知道了它的組成,就能夠去給它賦值了,只不過賦值的過程相反,是從裏到外的,先建立裏面,再逐層建立外面。

第一步,建立最裏面的List:

// new一個ArrayList對象
List inside = new ArrayList<String> ();

// 添加一個String元素
inside.add("This is test String");

第二步,把這個有值的List,放到一個Map中

// new一個HashMap對象
Map middle = new HashMap<String, List<String>> ();

// 添加第一步的List元素
middle.add("key", inside);

這樣就獲得了一個有值的Map。
第三步,把這個Map賦值給外層的List。

// new一個ArrayList對象,這就是題目中的list變量
List list = new ArrayList< Map< String, List<String> > >();

// 添加第二步的Map元素
outside.add(middle);

至此,一個集合套娃已完成。

總結

見到泛型別煩惱,
這個功能很是好。
泛型若想用得好,
關鍵在於尖括號。

若是用函數來類比:

  • 函數是使用相同的方法,經過改變數據來產生不一樣結果。
  • 回調是使用相同的數據,經過改變方法來產生不一樣的結果。
  • 泛型是使用相同的類,經過改變對象的類型來產生不一樣的結果。
相關文章
相關標籤/搜索