深刻理解Java泛型

泛型是什麼

一說到泛型,大夥確定不會陌生,咱們代碼裏面有不少相似這樣的語句:java

List<String> list=new ArrayList<>();
複製代碼

ArrayList就是個泛型類,咱們經過設定不一樣的類型,能夠往集合裏面存儲不一樣類型的數據類型(並且只能存儲設定的數據類型,這是泛型的優點之一)。「泛型」簡單的意思就是泛指的類型(參數化類型)。想象下這樣的場景:若是咱們如今要寫一個容器類(支持數據增刪查詢的),咱們寫了支持String類型的,後面還須要寫支持Integer類型的。而後呢?Doubel、Float、各類自定義類型?這樣重複代碼太多了,並且這些容器的算法都是一致的。咱們能夠經過泛指一種類型T,來代替咱們以前須要的全部類型,把咱們須要的類型做爲參數傳遞到容器裏面,這樣咱們算法只須要寫一套就能夠適應全部的類型。最典型的的例子就是ArrayList了,這個集合咱們不管傳遞什麼數據類型,它都能很好的工做。
聰明的同窗看完上面的描述,靈機一動,寫出了下面的代碼:算法

class MyList{
    private Object[] elements=new Object[10];
    private int size;
    
    public void add(Object item) {
    	elements[size++]=item;
    }
    
    public Object get(int index) {
    	return elements[index];
    }
}
複製代碼

這個代碼靈活性很高,全部的類型均可以向上轉型爲Object類,這樣咱們就能夠往裏面存儲各類類型的數據了。的確Java在泛型出現以前,也是這麼作的。可是這樣的有一個問題:若是集合裏面數據不少,某一個數據轉型出現錯誤,在編譯期是沒法發現的。可是在運行期會發生java.lang.ClassCastException。例如:數組

MyList myList=new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));
複製代碼

咱們在這個集合裏面存儲了多個類型(某些狀況下容器可能會存儲多種類型的數據),若是數據量較多,轉型的時候不免會出現異常,而這些都是沒法在編譯期得知的。而泛型一方面讓咱們只能往集合中添加一種類型的數據,同時可讓咱們在編譯期就發現這些錯誤,避免運行時異常的發生,提高代碼的健壯性。bash

Java泛型介紹

下面咱們來介紹Java泛型的相關內容,下面會介紹如下幾個方面:ide

  • Java泛型類
  • Java泛型方法
  • Java泛型接口
  • Java泛型擦除及其相關內容
  • Java泛型通配符

Java泛型類

類結構是面向對象中最基本的元素,若是咱們的類須要有很好的擴展性,那麼咱們能夠將其設置成泛型的。假設咱們須要一個數據的包裝類,經過傳入不一樣類型的數據,能夠存儲相應類型的數據。咱們看看這個簡單的泛型類的設計:函數

class DataHolder<T>{
    T item;
    
    public void setData(T t) {
    	this.item=t;
    }
    
    public T getData() {
    	return this.item;
    }
}
複製代碼

泛型類定義時只須要在類名後面加上類型參數便可,固然你也能夠添加多個參數,相似於<K,V>,<T,E,K>等。這樣咱們就能夠在類裏面使用定義的類型參數。
泛型類最經常使用的使用場景就是「元組」的使用。咱們知道方法return返回值只能返回單個對象。若是咱們定義一個泛型類,定義2個甚至3個類型參數,這樣咱們return對象的時候,構建這樣一個「元組」數據,經過泛型傳入多個對象,這樣咱們就能夠一次性方法多個數據了。ui

Java泛型方法

前面咱們介紹的泛型是做用於整個類的,如今咱們來介紹泛型方法。泛型方法既能夠存在於泛型類中,也能夠存在於普通的類中。若是使用泛型方法能夠解決問題,那麼應該儘可能使用泛型方法。下面咱們經過例子來看一下泛型方法的使用:this

class DataHolder<T>{
    T item;
    
    public void setData(T t) {
    	this.item=t;
    }
    
    public T getData() {
    	return this.item;
    }
    
    /**
     * 泛型方法
     * @param e
     */
    public <E> void PrinterInfo(E e) {
    	System.out.println(e);
    }
}
複製代碼

咱們來看運行結果:spa

1
AAAAA
8.88
複製代碼

從上面的例子中,咱們看到咱們是在一個泛型類裏面定義了一個泛型方法printInfo。經過傳入不一樣的數據類型,咱們均可以打印出來。在這個方法裏面,咱們定義了類型參數E。這個E和泛型類裏面的T二者之間是沒有關係的。哪怕咱們將泛型方法設置成這樣:設計

//注意這個T是一種全新的類型,能夠與泛型類中聲明的T不是同一種類型。
public <T> void PrinterInfo(T e) {
    System.out.println(e);
}
//調用方法
DataHolder<String> dataHolder=new DataHolder<>();
dataHolder.PrinterInfo(1);
dataHolder.PrinterInfo("AAAAA");
dataHolder.PrinterInfo(8.88f);
複製代碼

這個泛型方法依然能夠傳入Double、Float等類型的數據。泛型方法裏面的類型參數T和泛型類裏面的類型參數是不同的類型,從上面的調用方式,咱們也能夠看出,泛型方法printInfo不受咱們DataHolder中泛型類型參數是String的影響。 咱們來總結下泛型方法的幾個基本特徵:

  • public與返回值中間很是重要,能夠理解爲聲明此方法爲泛型方法。
  • 只有聲明瞭的方法纔是泛型方法,泛型類中的使用了泛型的成員方法並非泛型方法。
  • 代表該方法將使用泛型類型T,此時才能夠在方法中使用泛型類型T。
  • 與泛型類的定義同樣,此處T能夠隨便寫爲任意標識,常見的如T、E、K、V等形式的參數經常使用於表示泛型。

Java泛型接口

Java泛型接口的定義和Java泛型類基本相同,下面是一個例子:

//定義一個泛型接口
public interface Generator<T> {
    public T next();
}
複製代碼

此處有兩點須要注意:

  • 泛型接口未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一塊兒加到類中。例子以下:
/* 即:class DataHolder implements Generator<T>{
 * 若是不聲明泛型,如:class DataHolder implements Generator<T>,編譯器會報錯:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
複製代碼
  • 若是泛型接口傳入類型參數時,實現該泛型接口的實現類,則全部使用泛型的地方都要替換成傳入的實參類型。例子以下:
class DataHolder implements Generator<String>{
    @Override
    public String next() {
    	return null;
    }
}
複製代碼

從這個例子咱們看到,實現類裏面的全部T的地方都須要實現爲String。

Java泛型擦除及其相關內容

咱們下面看一個例子:

Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1);		//class java.util.ArrayList
System.out.println(class2);		//class java.util.ArrayList
System.out.println(class1.equals(class2));	//true
複製代碼

咱們看輸出發現,class1和class2竟然是同一個類型ArrayList,在運行時咱們傳入的類型變量String和Integer都被丟掉了。Java語言泛型在設計的時候爲了兼容原來的舊代碼,Java的泛型機制使用了「擦除」機制。咱們來看一個更完全的例子:

class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}
//調用代碼及輸出
List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
/** 
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
 */
複製代碼

上面的代碼裏,咱們想在運行時獲取類的類型參數,可是咱們看到返回的都是「形參」。在運行期咱們是獲取不到任何已經聲明的類型信息的。
注意:
編譯器雖然會在編譯過程當中移除參數的類型信息,可是會保證類或方法內部參數類型的一致性。
泛型參數將會被擦除到它的第一個邊界(邊界能夠有多個,重用 extends 關鍵字,經過它能給與參數類型添加一個邊界)。編譯器事實上會把類型參數替換爲它的第一個邊界的類型。若是沒有指明邊界,那麼類型參數將被擦除到Object。下面的例子中,能夠把泛型參數T看成HasF類型來使用。

public interface HasF {
    void f();
}

public class Manipulator<T extends HasF> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}
複製代碼

extend關鍵字後後面的類型信息決定了泛型參數能保留的信息。Java類型擦除只會擦除到HasF類型。

Java泛型擦除的原理

咱們經過例子來看一下,先看一個非泛型的版本:

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String s = (String) holder.getObj();
    }
}
// SimpleHolder.class
public class SimpleHolder {
  public SimpleHolder();
    Code:
       0: aload_0       
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return        

  public java.lang.Object getObj();
    Code:
       0: aload_0       
       1: getfield      #2 // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(java.lang.Object);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2 // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3 // class SimpleHolder
       3: dup           
       4: invokespecial #4 // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5 // String Item
      11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8 // class java/lang/String
      21: astore_2      
      22: return        
}
複製代碼

下面咱們給出一個泛型的版本,從字節碼的角度來看看:

//GenericHolder.java
public class GenericHolder<T> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setObj("Item");
        String s = holder.getObj();
    }
}

//GenericHolder.class
public class GenericHolder<T> {
  T obj;

  public GenericHolder();
    Code:
       0: aload_0       
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return        

  public T getObj();
    Code:
       0: aload_0       
       1: getfield      #2 // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(T);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2 // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3 // class GenericHolder
       3: dup           
       4: invokespecial #4 // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5 // String Item
      11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8 // class java/lang/String
      21: astore_2      
      22: return        
}
複製代碼

在編譯過程當中,類型變量的信息是能拿到的。因此,set方法在編譯器能夠作類型檢查,非法類型不能經過編譯。可是對於get方法,因爲擦除機制,運行時的實際引用類型爲Object類型。爲了「還原」返回結果的類型,編譯器在get以後添加了類型轉換。因此,在GenericHolder.class文件main方法主體第18行有一處類型轉換的邏輯。它是編譯器自動幫咱們加進去的。
因此在泛型類對象讀取和寫入的位置爲咱們作了處理,爲代碼添加約束。

Java泛型擦除的缺陷及補救措施

泛型類型不能顯式地運用在運行時類型的操做當中,例如:轉型、instanceof 和 new。由於在運行時,全部參數的類型信息都丟失了。相似下面的代碼都是沒法經過編譯的:

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        //編譯不經過
        if (arg instanceof T) {
        }
        //編譯不經過
        T var = new T();
        //編譯不經過
        T[] array = new T[SIZE];
        //編譯不經過
        T[] array = (T) new Object[SIZE];
    }
}
複製代碼

那咱們有什麼辦法來補救呢?下面介紹幾種方法來一一解決上面出現的問題。

類型判斷問題

咱們能夠經過下面的代碼來解決泛型的類型信息因爲擦除沒法進行類型判斷的問題:

/**
 * 泛型類型判斷封裝類
 * @param <T>
 */
class GenericType<T>{
    Class<?> classType;
    
    public GenericType(Class<?> type) {
        classType=type;
    }
    
    public boolean isInstance(Object object) {
        return classType.isInstance(object);
    }
}
複製代碼

在main方法咱們能夠這樣調用:

GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));
複製代碼

咱們經過記錄類型參數的Class對象,而後經過這個Class對象進行類型判斷。

建立類型實例

泛型代碼中不能new T()的緣由有兩個,一是由於擦除,不能肯定類型;而是沒法肯定T是否包含無參構造函數。
爲了不這兩個問題,咱們使用顯式的工廠模式:

/**
 * 使用工廠方法來建立實例
 *
 * @param <T>
 */
interface Factory<T>{
    T create();
}

class Creater<T>{
    T instance;
    public <F extends Factory<T>> T newInstance(F f) {
    	instance=f.create();
    	return instance;
    }
}

class IntegerFactory implements Factory<Integer>{
    @Override
    public Integer create() {
    	Integer integer=new Integer(9);
    	return integer;
    }
}
複製代碼

咱們經過工廠模式+泛型方法來建立實例對象,上面代碼中咱們建立了一個IntegerFactory工廠,用來建立Integer實例,之後代碼有變更的話,咱們能夠添加新的工廠類型便可。
調用代碼以下:

Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));
複製代碼
建立泛型數組

通常不建議建立泛型數組。儘可能使用ArrayList來代替泛型數組。可是在這裏仍是給出一種建立泛型數組的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        
    }
}
複製代碼

這裏咱們使用的仍是傳參數類型,利用類型的newInstance方法建立實例的方式。

Java泛型的通配符

上界通配符<? extends T>

咱們先來看一個例子:

class Fruit {}
class Apple extends Fruit {}
複製代碼

如今咱們定義一個盤子類:

class Plate<T>{
    T item;
    public Plate(T t){
        item=t;
    }
    
    public void set(T t) {
        item=t;
    }
    
    public T get() {
        return item;
    }
}
複製代碼

下面,咱們定義一個水果盤子,理論上水果盤子裏,固然能夠存在蘋果

Plate<Fruit> p=new Plate<Apple>(new Apple());
複製代碼

你會發現這段代碼沒法進行編譯。裝蘋果的盤子」沒法轉換成「裝水果的盤子:

cannot convert from Plate<Apple> to Plate<Fruit>
複製代碼

從上面代碼咱們知道,就算容器中的類型之間存在繼承關係,可是Plate和Plate兩個容器之間是不存在繼承關係的。 在這種狀況下,Java就設計成Plate<? extend Fruit>來讓兩個容器之間存在繼承關係。咱們上面的代碼就能夠進行賦值了

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
複製代碼

Plate<? extend Fruit>是Plate< Fruit >和Plate< Apple >的基類。
咱們經過一個更加詳細的例子來看一下上界的界限:

class Food{}

class Fruit extends Food {}
class Meat extends Food {}

class Apple extends Fruit {}
class Banana extends Fruit {}
class Pork extends Meat{}
class Beef extends Meat{}

class RedApple extends Apple {}
class GreenApple extends Apple {}
複製代碼

在上面這個類層次中,Plate<? extend Fruit>,覆蓋下面的藍色部分:

若是咱們往盤子裏面添加數據,例如:

p.set(new Fruit());
p.set(new Apple());
複製代碼

你會發現沒法往裏面設置數據,按道理說咱們將泛型類型設置爲? extend Fruit。按理說咱們往裏面添加Fruit的子類應該是能夠的。可是Java編譯器不容許這樣操做。<? extends Fruit>會使往盤子裏放東西的set()方法失效。但取東西get()方法還有效
緣由是:
Java編譯期只知道容器裏面存放的是Fruit和它的派生類,具體是什麼類型不知道,多是Fruit?多是Apple?也多是Banana,RedApple,GreenApple?編譯器在後面看到Plate< Apple >賦值之後,盤子裏面沒有標記爲「蘋果」。只是標記了一個佔位符「CAP#1」,來表示捕獲一個Fruit或者Fruit的派生類,具體是什麼類型不知道。全部調用代碼不管往容器裏面插入Apple或者Meat或者Fruit編譯器都不知道能不能和這個「CAP#1」匹配,因此這些操做都不容許。
最新理解:
一個Plate<? extends Fruit>的引用,指向的多是一個Plate類型的盤子,要往這個盤子裏放Banana固然是不被容許的。個人一個理解是:Plate<? extends Fruit>表明某個只能放某種類型水果的盤子,而不是什麼水果都能往裏放的盤子
可是上界通配符是容許讀取操做的。例如代碼:

Fruit fruit=p.get();
Object object=p.get();
複製代碼

這個咱們很好理解,因爲上界通配符設定容器中只能存放Fruit及其派生類,那麼獲取出來的咱們均可以隱式的轉爲其基類(或者Object基類)。因此上界描述符Extends適合頻繁讀取的場景。

下界通配符<? super T>

下界通配符的意思是容器中只能存放T及其T的基類類型的數據。咱們仍是以上面類層次的來看,<? super Fruit>覆蓋下面的紅色部分:

下界通配符<? super T>不影響往裏面存儲,可是讀取出來的數據只能是Object類型。
緣由是:
下界通配符規定了元素最小的粒度,必須是T及其基類,那麼我往裏面存儲T及其派生類都是能夠的,由於它均可以隱式的轉化爲T類型。可是往外讀就很差控制了,裏面存儲的都是T及其基類,沒法轉型爲任何一種類型,只有Object基類才能裝下。

PECS原則

最後簡單介紹下Effective Java這本書裏面介紹的PECS原則。

  • 上界<? extends T>不能往裏存,只能往外取,適合頻繁往外面讀取內容的場景。
  • 下界<? super T>不影響往裏存,但往外取只能放在Object對象裏,適合常常往裏面插入數據的場景。

<?>無限通配符

無界通配符 意味着可使用任何對象,所以使用它相似於使用原生類型。但它是有做用的,原生類型能夠持有任何類型,而無界通配符修飾的容器持有的是某種具體的類型。舉個例子,在List<\?>類型的引用中,不能向其中添加Object, 而List類型的引用就能夠添加Object類型的變量。
最後提醒一下的就是,List<\Object>與List<?>並不等同,List<\Object>是List<?>的子類。還有不能往List<?> list裏添加任意對象,除了null。

相關文章
相關標籤/搜索