Java核心知識1:泛型機制詳解

1 理解泛型的本質

JDK 1.5開始引入Java泛型(generics)這個特性,該特性提供了編譯時類型安全檢測機制,允許程式設計師在編譯時檢測到非法的類型。
泛型的本質是參數化類型,即給類型指定一個參數,然後在使用時再指定此參數具體的值,那樣這個類型就可以在使用時決定了。這種參數類型可以用在類、介面和方法中,分別被稱為泛型類、泛型介面、泛型方法。
為了兼容之前的版本,Java泛型的實現採取了「偽泛型」的策略,即Java在語法上支援泛型,但是在編譯階段會進行所謂的「類型擦除」(Type Erasure),將所有的泛型表示(尖括弧中的內容)都替換為具體的類型(其對應的原生態類型)。
image

2 泛型的作用

泛型有四個作用:類型安全、自動轉換、性能提升、可復用性。即在編譯的時候檢查類型安全,將所有的強制轉換都自動和隱式進行,同時提高程式碼的可復用性。
image

2.1 泛型如何保證類型安全

在沒有泛型之前,從集合中讀取到的每一個對象都必須進行類型轉換,如果不小心插入了錯誤的類型對象,在運行時的轉換處理就會出錯。
比如:沒有泛型的情況下使用集合:

public static void noGenericTest() {
        // 編譯正常通過,但是使用的時候可能轉換處理出現問題
        ArrayList arr = new ArrayList();
        arr.add("加入一個字元串");
        arr.add(1);
        arr.add('a');
    }

有泛型的情況下使用集合:

public static void genericTest() {
        // 編譯不通過,直接提示異常,Required type:String
        ArrayList<String> arr = new ArrayList<>();
        arr.add("加入一個字元串");
        arr.add(1);
        arr.add('a');
    }

有了泛型後,會對類型進行驗證,所以集合arr在編譯的時候add(1)、add(‘a’) 都會編譯不通過。
這個過程相當於告訴編譯器每個集合接收的對象類型是什麼,編譯器在編譯期就會做類型檢查,告知是否插入了錯誤類型的對象,使得程式更加安全,增強了程式的健壯性。

2.2 類型自動轉換,消除強轉

泛型的另一個好處是消除源程式碼中的強制類型轉換,這樣程式碼可讀性更強,且減少了轉換類型出錯的可能性。
以下面的程式碼為例子,以下程式碼段需要強制轉換,否則編譯會通不過:

ArrayList list  = new ArrayList();
list.add(1);
int i = (int) list.get(0);  // 需強轉

當重寫為使用泛型時,程式碼不需要強制轉換:

ArrayList<Integer> list  = new ArrayList<>();
list.add(1);
int i = list.get(0);  // 無需轉換

2.3 避免裝箱、拆箱,提高性能

在非泛型編程中,將筒單類型作為Object傳遞時會引起Boxing(裝箱)和Unboxing(拆箱)操作,這兩個過程都是具有很大開銷的。引入泛型後,就不必進行Boxing和Unboxing操作了,所以運行效率相對較高,特別在對集合操作非常頻繁的系統中,這個特點帶來的性能提升更加明顯。
泛型變數固定了類型,使用的時候就已經知道是值類型還是引用類型,避免了不必要的裝箱、拆箱操作。

object a=1;//由於是object類型,會自動進行裝箱操作。
 
int b=(int)a;//強制轉換,拆箱操作。這樣一去一來,當次數多了以後會影響程式的運行效率。

使用泛型後

public static T GetValue<T>(T a) {
  return a;
}
 
public static void Main(){
  int b=GetValue<int>(1);//使用這個方法的時候已經指定了類型是int,所以不會有裝箱和拆箱的操作。
}

2.4 提升程式可復用性

引入泛型的另一個意義在於:適用於多種數據類型執行相同的程式碼(程式碼復用)
我們通過下面的例子來說明,程式碼如下:

private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

如果沒有泛型,要實現不同類型的加法,每種類型都需要重載一個add方法;通過泛型,我們可以復用為一個方法:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}

3 泛型的使用

3.1 泛型類

泛型類是指把泛型定義在類上,具體的定義格式如下:

public class 類名 <泛型類型1,...> {
// todo
}

注意事項:泛型類型必須是引用類型,非基本數據類型
定義泛型類,在類名後添加一對尖括弧,並在尖括弧中填寫類型參數,參數可以有多個,多個參數使用逗號分隔:

public class GenericClass<ab,a,c> {
  // todo
}

當然,這個後面的參數類型也是有規範的,不能像上面一樣隨意,通常類型參數我們都使用大寫的單個字母表,可以任意指定,但是還是建議使用有字面含義的,讓人通俗易懂,下面的字母可以參考使用:

T:任意類型 type
E:集合中元素的類型 element
K:key-value形式 key
V: key-value形式 value
N: Number(數值類型)
?: 表示不確定的java類型

這邊舉個例子,假設我們寫一個通用的返回對象,對象中的某個欄位的類型不定:

@Data
public class Response<T> {
    /**
     * 狀態
     */
    private boolean status;
    /**
     * 編碼
     */
    private Integer code;
    /**
     * 消息
     */
    private String msg;
    /**
     * 介面返回內容,不同的介面返回的內容不一致,使用泛型數據
     */
    private T data;

    /**
     * 構造
     * @param status
     * @param code
     * @param msg
     * @param data
     */
    public Response(boolean status,int code,String msg,T data) {
        this.status = status;
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

做成泛型類,他的通用性就很強了,這時候他返回的情況可能如下:
先定義一個用戶資訊對象

@Data
public class UserInfo {
    /**
     * 用戶編號
     */
    private String userCode;
    /**
     * 用戶名稱
     */
    private String userName;
}

嘗試返回不同的數據類型:

        /**
         * 返回字元串
         */
        Response<String> responseStr = new Response<>(true,200,"success","Hello Word");

        /**
         * 返回用戶對象
         */
        UserInfo userInfo = new UserInfo();
        userInfo.setUserCode("123456");
        userInfo.setUserName("Brand");
        Response<UserInfo> responseObj = new Response<>(true,200,"success",userInfo);

輸出結果如下:

{
	"status": true,
	"code": 200,
	"msg": "success",
	"data": "Hello Word"
}
// 和
{
	"status": true,
	"code": 200,
	"msg": "success",
	"data": {
		"user_code": "123456",
		"user_name": "Brand"
	}
}

3.2 泛型介面

泛型方法概述:把泛型定義在借口上,他的格式如下

public interface 介面名<T> {
  // todo
}

注意點1:方法聲明中定義的形參只能在該方法里使用,而介面、類聲明中定義的類型形參則可以在整個介面、類中使用。當調用fun()方法時,根據傳入的實際對象,編譯器就會判斷出類型形參T所代表的實際類型。

public interface GenericInterface<T> {
void show(T value);}
}
public class StringShowImpl implements GenericInterface<String> {
@Override
public void show(String value) {
System.out.println(value);
}}
 
public class NumberShowImpl implements GenericInterface<Integer> {
@Override
public void show(Integer value) {
System.out.println(value);
}}

注意點2:使用泛型的時候,前後定義的泛型類型必須保持一致,否則會出現編譯異常:

// 編譯的時候會報錯,因為前後類型不一致
GenericInterface<String> genericInterface = new NumberShowImpl();
// 編譯正常,前面泛型介面不指定類型,由new後面的實例化來推導。
GenericInterface g1 = new NumberShowImpl();
GenericInterface g2 = new StringShowImpl();

3.3 泛型方法

泛型方法,是在調用方法的時候指明泛型的具體類型 。定義格式如下:

public <泛型類型> 返回類型 方法名(泛型類型 變數名) {
   // todo
}

舉例說明,下面是一個典型的泛型方法,根據傳入的對象,列印它的值和類型:

/**
     * 泛型方法    
     * @param <T> 泛型的類型
	  * @param c 傳入泛型的參數對象
     * @return T 返回值為T類型
     * 說明:
     *   1)public 與 返回值中間<T>非常重要,可以理解為聲明此方法為泛型方法。
     *   2)只有聲明了<T>的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。
     *   3)<T>表明該方法將使用泛型類型T,此時才可以在方法中使用泛型類型T。
     *   4)與泛型類的定義一樣,此處T可以隨便寫為任意標識,常見的如T、E等形式的參數常用於表示泛型。
     */
    public <T> T genercMethod(T c) {
        System.out.println(c.getClass());
        System.out.println(c);
        return c;
   } 
 
public static void main(String[] args) {
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("Hello World"); //這裡的泛型跟下面調用的泛型方法可以不一樣。
    String str = genericString.genercMethod("brand");//傳入的是String類型,返回的也是String類型
    Integer i = genericString.genercMethod(100);//傳入的是Integer類型,返回的也是Integer類型
}

輸出結果如下:

class java.lang.String
brand 
 
class java.lang.Integer
100

從上面可以看出,泛型方法隨著我們的傳入參數類型不同,執行的效果不同,拿到的結果也不一樣。泛型方法能使方法獨立於類而產生變化。

3.4 泛型通配符(上下界)

Java泛型的通配符是用於解決泛型之間引用傳遞問題的特殊語法, 主要有以下三類:

  • 無邊界的通配符,使用精確的參數類型
  • 關鍵字聲明了類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類
  • 關鍵字聲明了類型的下界,表示參數化的類型可能是指定的類型,或者是此類型的父類

結構如下:

// 表示類型參數可以是任何類型
public class B<?> {
}
 
// 上界:表示類型參數必須是A或者是A的子類
public class B<T extends A> {
}
 
// 下界:表示類型參數必須是A或者是A的超類型
public class B<T supers A> {
}

上界示例:

class Info<T extends Number>{    // 此處泛型只能是數字類型
    private T var ;        // 定義泛型變數
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接列印
        return this.var.toString() ;
    }
}
public class demo1{
    public static void main(String args[]){
        Info<Integer> i1 = new Info<Integer>() ;        // 聲明Integer的泛型對象
    }
}

下界示例:

class Info<T>{
    private T var ;        // 定義泛型變數
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接列印
        return this.var.toString() ;
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>() ;        // 聲明String的泛型對象
        Info<Object> i2 = new Info<Object>() ;        // 聲明Object的泛型對象
        i1.setVar("hello") ;
        i2.setVar(new Object()) ;
        fun(i1) ;
        fun(i2) ;
    }
    public static void fun(Info<? super String> temp){    // 只能接收String或Object類型的泛型,String類的父類只有Object類
        System.out.print(temp + ", ") ;
    }
}

4 泛型實現原理

Java泛型這個特性是從JDK 1.5才開始加入的,因此為了兼容之前的版本,Java泛型的實現採取了「偽泛型」的策略,即Java在語法上支援泛型,但是在編譯階段會進行所謂的「類型擦除」(Type Erasure),
將所有的泛型表示(尖括弧中的內容)都替換為具體的類型(其對應的原生態類型),就像完全沒有泛型一樣。
泛型本質是將數據類型參數化,它通過擦除的方式來實現,即編譯器會在編譯期間「擦除」泛型語法並相應的做出一些類型轉換動作。

4.1 泛型的類型擦除原則

  • 消除類型參數聲明,即刪除<>及其包圍的部分。
  • 根據類型參數的上下界推斷並替換所有的類型參數為原生態類型:如果類型參數是無限制通配符或沒有上下界限定則替換為Object,如果存在上下界限定則根據子類替換原則取類型參數的最左邊限定類型(即父類)。
  • 為了保證類型安全,必要時插入強制類型轉換程式碼。
  • 自動產生「橋接方法」以保證擦除類型後的程式碼仍然具有泛型的「多態性」。

4.2 擦除的方式

擦除類定義中的類型參數 – 無限制類型擦除
當類定義中的類型參數沒有任何限制時,在類型擦除中直接被替換為Object,即形如和<?>的類型參數都被替換為Object。
image

擦除類定義中的類型參數 – 有限制類型擦除
當類定義中的類型參數存在限制(上下界)時,在類型擦除中替換為類型參數的上界或者下界,比如形如和<? extends Number>的類型參數被替換為Number,<? super Number>被替換為Object。
image

擦除方法定義中的類型參數
擦除方法定義中的類型參數原則和擦除類定義中的類型參數是一樣的,這裡僅以擦除方法定義中的有限制類型參數為例。
image

Tags: