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

1 理解泛型的本質

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

2 泛型的作用

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

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 泛型的類型擦除原則

4.2 擦除的方式

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

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IcM8lVscMqczFi0jeB5wEA