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

Brand發表於2022-04-11

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

相關文章