Java 泛型學習總結

前言

Java 5 添加了泛型,提供了編譯時類型安全檢測機制,該機制容許程序員在編譯時檢測到非法的類型。
泛型的本質是參數化類型,能夠爲之前處理通用對象的類和方法,指定具體的對象類型。聽起來有點抽象,因此咱們將立刻看一些泛型用在集合上的例子:javascript

泛型集合

先看一個沒有泛型的集合例子:html

List list = new ArrayList();
list.add(new Integer(2));
list.add("a String");

由於 List 能夠添加任何對象,因此從 List 中取出的對象時,由於不肯定(List不記住元素類型)當時候保存進 List 的元素類型,這個對象的類型只能是 Object ,還必須由程序編寫者記住添加元素類型,而後取出時再進行強制類型轉換就,以下:java

Integer integer = (Integer) list.get(0);
String string   = (String) list.get(1);

一般,咱們只使用帶有單一類型元素的集合,而且不但願其餘類型的對象被添加到集合中,例如,只有 Integer 的 List ,不但願將 String 對象放進集合,而且也不想本身記住元素類型,而且強制類型轉換還可能會出現錯誤。程序員

使用 Generics(泛型)就能夠設置集合的類型,以限制能夠將哪一種對象插入集合中。 這能夠確保集合中的元素,都是同一種已知類型的,所以取出數據的時候就沒必要進行強制類型轉換了,下面是一個 String 類型的 List 的使用例子:數組

List<String> strings = new ArrayList<String>();
strings.add("a String");
String aString = strings.get(0);

以上這個 List 集合只能放入 String 對象,若是視圖放入別的對象那麼編譯器會報錯,讓代碼編寫者必須進行處理,這就是額外的類型檢查。另外注意List 的 <> 尖括號裏面只能是對象引用類型,不包括基本類型(可使用對應包裝類代替)。 安全

Java 泛型從 Java 7 開始,編譯器能夠自動類型判斷,能夠省略構造器中的泛型,下面是一個Java 7 泛型例子:markdown

List<String> strings = new ArrayList<>();

這也叫作菱形語法(<>), 在上面的示例中,實例化 ArrayList 的時候,編譯器根據前面 List 知道 new 的 ArrayList 泛型信息是 String。 數據結構

foreach 循環能夠很好地與泛型集合整合,以下:async

List<String> strings = new ArrayList<>();

// 這裏省略將 String 元素添加進集合的代碼...

for(String aString : strings){
  System.out.println(aString);
}

也可使用泛型迭代器遍歷集合,以下:ide

List<String> list = new ArrayList<String>;

Iterator<String> iterator = list.iterator();

while(iterator.hasNext()){
  String aString = iterator.next();
}

注意,泛型類型檢查僅在編譯時存在。在運行時,可使用反射或者其餘方式使字符串集合插入其餘對象,但通常不會這樣作。

固然泛型的用處不只僅限於集合。

泛型類

從上面的內容中,咱們已瞭解泛型的大概理念。
能夠在自定義 Java 類上使用泛型,並不侷限於 Java API 中的預約義類。定義泛型類只須要在類名後緊跟 尖括號,其中 T 是類型標識符,也能夠是別的,好比 E 、V 等,以下例:

public class GenericFactory<T> {
    Class theClass = null;

    public GenericFactory(Class theClass) {
        this.theClass = theClass;
    }

    public T createInstance() throws Exception {
        return (T) this.theClass.newInstance();
    }
}

其中 Class.newInstance() 方法是反射知識的內容,這裏只要知道此方法用於 theClass 類對象的建立就行。
是一個類型標記,表示這個泛型類在實例化時能夠擁有的類型集。下面是一個例子:

GenericFactory<MyClass> factory = new GenericFactory<MyClass>(MyClass.class);
MyClass myClassInstance = factory.createInstance();

GenericFactory<SomeObject> factory1 = new GenericFactory<SomeObject>(SomeObject.class);
SomeObject someObjectInstance = factory1.createInstance();

使用泛型,咱們就沒必要轉換從 factory.createInstance() 方法返回的對象,會自動返回 new GenericFactory 的 <> 尖括號中的類型對象。

泛型方法

一個泛型方法定義以下:

public static <T> T addAndReturn(T element, Collection<T> collection){
    collection.add(element);
    return element;
}

其中 T 是方法返回值, 放在方法返回值以前,是全部泛型方法必須有的類型參數聲明,表示這是一個泛型方法,若是沒有 只有 T ,那就不是泛型方法,而是泛型類的一個成員方法,就像上面泛型類定義的方法:

public T createInstance() throws Exception { ... }

這不是一個泛型方法,而是 GenericFactory 泛型類的一個成員方法而已,泛型方法必須在方法返回值以前有似於 的標記。

注意在泛型方法中,參數有 2 個,第一個是 T 類型的參數,第二個是元素類型爲 T 的 Collection 集合,編譯器會根據實際參數來推斷 T 爲什麼種類型 ,以下這樣是徹底可行的:

String stringElement = "stringElement";
List<Object> objectList = new ArrayList<Object>();

Object theElement = addAndReturn(stringElement, objectList);

如上,第一個參數爲 String 類型,第二個是 List 類型,貌似兩個 T 是不匹配的,可是這裏編譯器會自動將 String 轉換爲 Object 類型,並將 T 標識爲 Object 。

繼承關係的泛型參數

定義以下類:

public class A { }
public class B extends A { }
public class C extends A { }

新建並初始化以下類對象:

List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();

由於 A 是 B 和 C 類的共同父類,那麼 List 是否是 List 和 List 的共同父類嗎?換句話說,是否能夠將 List 引用指向 List 對象呢?或者反過來讓 List 引用指向 List 對象呢?

答案是不能夠,第一種狀況,父類引用指向子類對象,如將 List 引用指向 List 對象,先假設這是能夠的,有以下代碼:

List<A> listA = listB;

這會出現問題,由於 listA 是 List 類型,它能夠放入任何 A 對象做爲元素,能夠放入 B 對象,也能夠放入 C 對象,可是它實際指向的是一個 List 集合,List 集合中本應該只存儲 B 類對象的,如今卻不可預料地放入了 C 對象,這和當初的定義不相符 List<B> listB = new ArrayList<B>(); ,因此這種狀況是不容許的。

第二種狀況,讓 List 引用指向 List 對象,這樣確定也是不能夠的,由於已存在的 List 集合中,可能已經存在了 A 類的非 B 類子類對象,如 C 類對象,這樣也不能保證從 List 集合中取出的元素必定就是 B 對象。

可是,確實存在須要使用泛型繼承關係的狀況,看以下代碼:

public void processElements(List<A> elements){
   for(A o : elements){
      System.out.println(o.getValue()); // 這裏假設的 A 類存在 getValue() 方法
   }
}

咱們但願定義一個類,讓其能處理全部指定類型元素的 List ,如 processElements(List elements) 方法,既能夠處理 List ,也能夠處理 List ,可是如上所述, List 和 List 根本就不存在任何繼承關係。
這時就可使用類型通配符如 <?> 來完成此需求。

類型通配符

有 3 中類型通配符用法,以下代碼:

List<?> listUknown = new ArrayList<A>(); // 任何類型元素均可接受
List<? extends A> listUknown = new ArrayList<A>(); // 可接受 A 子類類型元素
List<? super A> listUknown = new ArrayList<A>(); // 可接受 A 父類類型元素

還能夠有多個限定,好比限定要同時可比較和可序列化,就可使用 <T extends Comparable & Serializable>

無邊界類型通配符<?>

List<?> 表示未知類型的 List 。 這能夠是 List,List 或 List 等。

因爲不知道 List 的類型,因此只能從集合中讀取,不能放入新元素,而且只能將讀取的元素視爲 Object 類型,以下例:

public void processElements(List<?> elements){
   for(Object o : elements){
      System.out.println(o);
   }
}

如今 processElements() 方法可使用任何泛型參數的 List 集合做爲參數,以下:

List<A> listA = new ArrayList<A>();
processElements(listA);

上界類型通配符<? extends A>

這樣將表示可接受的泛型參數,必須是 A 類或者 A 類的子類,A 類就是最大的範圍,因此叫作上界。
定義以下方法:

public void processElements(List<? extends A> elements){
   for(A a : elements){
      System.out.println(a.getValue());
   }
}

這樣,它能夠接收 List, List 或 List 集合做爲參數,以下:

List<A> listA = new ArrayList<A>();
processElements(listA);

List<B> listB = new ArrayList<B>();
processElements(listB);

List<C> listC = new ArrayList<C>();
processElements(listC);

但 processElements(List<? extends A> elements) 方法仍然沒法插入元素到 List 中,由於不知道傳入的 List 中的元素的具體類型,它多是 A, B 或 C 類 ,若是插入成功,那麼取出來的時候就沒法確認,強制轉換就會失敗。

可是能夠讀取元素,由於 List 裏面已保存的元素必定是 A 類或其子類對象,轉換成 A 類不會報錯。

下界類型通配符 <? super A>

這樣將表示可接受的泛型參數,必須是 A 類(A 類的子類也屬於 A 類)或者 A 類的父類,A 類就是最小的範圍,因此叫作下界。
當知道 List 中的元素確定是 A 類或 A 的父類時,能夠安全地將 A 類的對象或 A 的子類對象(例如 B 或 C)插入 List 中。 下面是一個例子:

public static void insertElements(List<? super A> list){
    list.add(new A());
    list.add(new B());
    list.add(new C());
}

此處插入的全部元素都是 A 類對象或 A 類的父類的對象。因爲 B 和 C 都繼承了 A ,若是 A 有一個父類,B 和 C 也將是該父類的對象。

如今可使用 List 或類型爲 A 的父類調用 insertElements() ,以下類:

List<A> listA = new ArrayList<A>();
insertElements(listA);

List<Object> listObject = new ArrayList<Object>();
insertElements(listObject);

可是,insertElements() 方法沒法從 List 中讀取元素,除非它將讀取到的對象強制轉換爲 Object 。 調用 insertElements() 時,List 中已經存在的元素能夠是 A 類或其父類的任何類型,但不知道它是哪一個類。 因爲 Java 全部的類都是 Object 的子類,所以若是將它們轉換爲 Object ,則能夠從列表中讀取對象。以下代碼是正確的:

Object object = list.get(0);

可是以下代碼是錯誤的,由於父類對象不能轉換爲子類對象:

A object = list.get(0);

泛型擦除

Java 語言中的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,而且在相應的地方插入了強制轉型代碼,所以,對於運行期的 Java 語言來講,ArrayList<int>與 ArrayList<String>就是同一個類,因此泛型技術其實是 Java 語言的一顆語法糖,這就是泛型擦除,基於這種方法實現的泛型稱爲僞泛型。

Java 的泛型是僞泛型,僅僅提供編譯時檢查,泛型確保了只要在編譯時不出現錯誤,運行時就不出現強制轉換異常。在編譯完成後,全部的泛型信息都會被擦除掉,泛型附帶的類型信息對 JVM 是不可見的。

當泛型碰見重載

定義 2 個分別處理 List 和 List 元素的 processElements() 函數,代碼以下:

public void processElements(List<String> elements) {
}
public void processElements(List<Integer> elements) {
}

編譯器會報以下錯誤:
Error:(12, 17) java: 名稱衝突: processElements(java.util.List<java.lang.Integer>)和processElements(java.util.List<java.lang.String>)具備相同疑符

意思就是咱們定義了 2 個重複的方法,即兩個方法的特徵簽名(方法簡單名和方法中各個參數在常量池中的字段符號引用的集合)是同樣的,說明雖然泛型參數不同,但到底 List 和 List 是一個東西,它們的原始類型是同樣的,都是同一個 List 類對象,可使用以下代碼驗證:

Class strListClass = new ArrayList<String>().getClass();
Class intListClass = new ArrayList<Integer>().getClass();
System.out.println(strListClass); // class java.util.ArrayList
System.out.println(intListClass); // class java.util.ArrayList
System.out.println(strListClass == intListClass); // ture

那麼把其中一個方法的返回值修改爲和另外一個不同,能夠重載成功嗎?

public String processElements(List<String> elements) {
}

很明顯,也會提示同樣的報錯,沒法經過編譯,由於方法返回值是不參與重載選擇的,那爲何呢?

由於調用一個函數,並不必定須要接受它返回值,若是能夠根據返回值重載,那麼調用 processElements(list); 時,編譯器並不知道指望返回的值是什麼,由於咱們根本就不須要返回值,因此編譯器就不知道具體要調用哪一個方法。

瞭解虛擬機的朋友可能知道,從 Class 文件方法表(method_info)的數據結構來看,方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名之中,因此返回值不參與重載選擇。

原始類型(raw type)就是擦除(crased)掉泛型信息後的真正類型,泛型參數信息會被擦除,運行時最終使用 Object 或具體的限定類。

Class strListClass = new ArrayList<String>().getClass(); 編譯後會變成 Class strListClass = new ArrayList().getClass();

Java 爲何只把泛型信息留在編譯階段?
Java不能實現真正泛型的緣由? - RednaxelaFX的回答 - 知乎

其餘

JDK 文檔中常常能看到 T、K、V、E、N 等類型參數,實際上這些符號隨意選擇使用也是沒問題的,甚至可使用如 A、B 等,但建議和 JDK 文檔風格保持一致,至少得讓人容易理解。
常見各符號的含義:

T:type
K:key
V:value
E:element
N:Number

泛型數組

在 Java 中不能建立一個確切泛型類型的數組,以下代碼是不容許的:

List<String>[] strLists = new ArrayList<String>[8];

將會報錯:Error:(11, 27) java: 建立泛型數組。

可是使用通配符建立泛型數組是能夠的,好比下面代碼是被容許的:

List<?>[] strLists = new ArrayList<?>[8];

爲何?

首先對於Java數組,數組必須明確知道內部元素的類型,並且編譯器會」記住「這個類型,每次往數組裏插入新元素都會進行類型檢查,不匹配會拋出java.lang.ArrayStoreException錯誤。更多信息可參考此文↓
Java 爲何不支持泛型數組-簡書


本文參考:
菜鳥教程
Java Generics Tutorial

posted @ 2018-12-10 14:58 czwbig 閱讀( ...) 評論( ...) 編輯 收藏

相關文章
相關標籤/搜索