List<E>的尖括號是什麼意思?
什麼是泛型?如何去理解它?
泛型有什麼優勢?面試
以上問題能夠在本文中找到答案。算法
在全部編程語言中,都有這麼一種東西,叫作「數組」,好比C++中就有數組。編程
可是呢,傳統數組最大的缺點就是:它的長度是固定的,若是不知道一個數組須要容納多大的數據量,編寫程序時就會很困難,數組建的太大了,就會浪費內存資源,數組過小了,容量不夠就會報錯。segmentfault
因此,前人發明了各類各樣的大小可變的數組,也就是「集合」,Collection。數組
Java集合類型一覽(選自《HeadFirstJava》):安全
正由於集合沒有固定的大小,而且有着許多很方便的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>去掉或者刪掉,會發生什麼呢?
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)
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類型的對象
這就是今天的話題,泛型。
泛型是什麼?從字面意思解釋:「泛」是普遍的,「型」是一種特定類型,連起來就是「普遍的特定類型的對象」。
說它普遍,不管是什麼對象,是隻要符合規定的類型,集合(ArrayList)就能夠處理它;
說它特定,是由於它必須嚴格符合約定的類型,不然就會被編譯器攔截下來,沒法經過編譯。
若是說集合是各類瓶子,那麼泛型就是對於瓶子的約束。這種約束是爲了安全,若是沒有了泛型的約束,集合就能夠容納任何類型的對象,啥均可以裝進去,就像把綿羊放進老虎的集合中。
那麼,怎麼使用泛型呢?
首先,不一樣於方法的參數,參數是針對於某個方法來講的,而泛型是針對一個類或對象來講的。
再解釋一下就是:把一個參數傳給某個方法,這個方法接收到參數以後,就能夠處理它;把一個泛型傳給某個類,這個類接收到這個泛型以後,就能夠new出一個只能處理這個泛型的對象。
因此泛型和參數有類似之處,關鍵點就是在尖括號<>里加上類。
泛型能夠用在集合中:
new ArrayList<String>
// 聲明stringList變量是字符串類型的集合 // 而且連接到一個new出來的對象上 ArrayList<String> stringList = new ArrayList<String>;
泛型還能用在方法的參數中:
// 此方法接收的參數是List集合, // 但必須是字符串類型的集合才能夠 public void printString (List<String> list) { ... }
爲一個集合添加數據:
// 添加: add // 刪除: remove // 其餘用法請參考源碼,寫的很清楚 array.add("ABC");
基本的用法說完了,那麼就來看看,泛型的世界裏有什麼規律。
在以前的《從零起步,真正理解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很豐富,沒有必要再去本身寫了。
已經講了不少知識,接下來來實踐一下,若是能明白這個實例,就真正理解泛型了。
有這麼一道面試題:
// 要求:給變量賦值 List<Map<String,List<String>>> list = newArrayList<Map<String,List<String>>>();
要理解這道題,咱們還須要一些知識的補充。
Java集合類型一覽(選自《HeadFirstJava》):
集合有這麼多種,可是有些是類,有些是接口。
衆所衆知,接口是不能New的,因此題目中的List和Map都是不能new的,但咱們能夠new它們的實現類,而後賦值給這個類型的變量。
接着看題,變量名是list,類型是List集合,泛型是<Map<String,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);
至此,一個集合套娃已完成。
見到泛型別煩惱,
這個功能很是好。
泛型若想用得好,
關鍵在於尖括號。
若是用函數來類比: