重拾Java(7)-泛型

以下內容是我對 Java 8 編程參考官方教程(第9版) 該書的讀書筆記

一、概述

泛型是在 JDK 1.5 引入的,泛型的意思是參數化類型,通過泛型可以創建以類型安全的方法使用各種類型數據的類、接口以及方法,能夠使一份算法獨立於特定的數據類型,然後將算法應用於各種數據類型而不需要做額外的各種。 Object是所有其他類的超類,Object引用變量可以引用所有類型的對象,因此通過操作Object類型的引用,Java總是可以操作一般化的類、接口以及方法,但它們不能以類型安全的方式進行工作。 泛型提供了以前缺失的類型安全性,並且不再需要顯式地使用強制類型轉換,所有的類型轉換都是自動和隱式進行的。

二、泛型示例

2.1、帶一個參數類型的泛型類

下面看一個簡單的泛型類 User

public class User<T> {

    private T t;

    public User(T t) {
        this.t = t;
    }

    public void showType() {
        System.out.println("T: " + t);
        System.out.println("Name: " + t.getClass().getName());
    }

}

其中,T是實際參數類型的佔位符,當創建對象時,就會將實際類型傳給 User。在 showType() 中輸出參數值和參數值的類對象名稱

public class GenericMain {

    public static void main(String[] args) {
        User<String> stringUser = new User<>("leavesC");
        stringUser.showType();

        System.out.println();

        User<Integer> integerUser = new User<>(24);
        integerUser.showType();
    }

}

運行結果

image

2.2、帶多個參數類型的泛型類

在泛型類中可以聲明多個類型參數,通過逗號分隔參數列表

public class User<T, K> {

    private T t;

    private K k;

    public User(T t, K k) {
        this.t = t;
        this.k = k;
    }

    public void showType() {
        System.out.println("T: " + t);
        System.out.println("T Name: " + t.getClass().getName());
        System.out.println("K: " + k);
        System.out.println("K Name: " + k.getClass().getName());
    }

}
public class GenericMain {

    public static void main(String[] args) {
        User<String, Integer> stringUser = new User<>("leavesC", 24);
        stringUser.showType();

        System.out.println();

        User<String, Double> integerUser = new User<>("葉應是葉", 24.0);
        integerUser.showType();
    }

}

運行結果

image

此處指定了兩個類型參數:T 和 K,在創建User對象時就需要爲之傳遞兩個類型參數,但不要求兩個類型參數必須是相同的類型

2.3、泛型接口

除了可以定義泛型類外,還可以定義泛型接口

例如,定義一個泛型接口

public interface Control<T> {

    T test();

}

在泛型類中實現泛型接口

public class User<T, K> implements Control<T> {

    private T t;

    private K k;

    public User(T t, K k) {
        this.t = t;
        this.k = k;
    }

    public void showType() {
        System.out.println("T: " + t);
        System.out.println("T Name: " + t.getClass().getName());
        System.out.println("K: " + k);
        System.out.println("K Name: " + k.getClass().getName());
    }

    @Override
    public T test() {
        return null;
    }
    
}

在非泛型類中實現泛型接口

public class Stats implements Control<String> {

    @Override
    public String test() {
        return null;
    }
    
}

三、泛型只能使用引用類型

當聲明泛型類的實例時,傳遞的類型參數必須是引用類型,不能使用基本類型 例如,對於 User 泛型類來說,以下聲明是非法的

User<int,double> user=new User<>(1,10,0);

IDE 會提示類型參數不能是基本數據類型

image

可以使用類型封裝器封裝基本類型來解決該問題

四、有界類型

對於 User 泛型類來說,可以使用任何類代替類型參數,但有時候我們也會有限制能夠傳遞給類型參數的類型的需求。例如,希望創建一個對於所有的數值類型都能夠計算平均值的方法,包括整數、單精度浮點數和雙精度浮點數等。

對於所有的數值類,比如 Integer 以及 Double,都是 Number 類的子類,而 Number 類定義了 doubleValue() 方法,所以所有的數值封裝器都可以使用該方法。但編譯器不知道我們試圖創建的是隻使用數值類型的 Stats 對象,所以 doubleValue() 方法對編譯器來說是未知方法,因此在試圖編譯時以下泛型類會報錯。

public class Stats<T> {

    private T[] numbers;

    public Stats(T[] numbers) {
        this.numbers = numbers;
    }

    public double average() {
        double sum = 0;
        for (T t : numbers) {
            //錯誤,因爲並不是每種類型參數都包含 doubleValue 方法
            //需要我們主動告訴編譯器我們傳入的類型參數都是Number類的子類
            sum += t.doubleValue();
        }
        return sum / numbers.length;
    }

}

爲了處理這種情況,需要爲泛型類提供有界類型。在指定類型參數時,可以創建聲明超類的上界,所有類型參數都必須派生自超類,通過關鍵字 extends 來聲明限制。

這樣,T 只能被 Number 類或其子類替代,阻止創建非數值類型的Stats對象

public class Stats<T extends Number> {

    private T[] numbers;

    public Stats(T[] numbers) {
        this.numbers = numbers;
    }

    public double average() {
        double sum = 0;
        for (T t : numbers) {
            sum += t.doubleValue();
        }
        return sum / numbers.length;
    }

}
public class GenericMain {

    public static void main(String[] args) {
        Integer[] intNumbers = {1, 2, 3, 4};
        Stats<Integer> integerStats = new Stats<>(intNumbers);
        System.out.println("int numbers average: " + integerStats.average());

        System.out.println();

        Double[] doubleNumbers = {1.0, 2.0, 3.0, 4.0};
        Stats<Double> doubleStats = new Stats<>(doubleNumbers);
        System.out.println("double numbers average: " + doubleStats.average());
    }

}

運行結果

image

此外,除了使用類作爲邊界外,邊界也可以包含一個類和一個或多個接口。聲明方式是:先指定類類型,再使用 & 運算符連接接口 例如

    class Test<T extends MyClass & MyInterface1 & MyInterface2>

五、使用通配符參數

5.1、一般用法

現在來爲上一節的 Stats 泛型類添加一個比較 Stats 對象之間包含的數組的平均值是否相等的方法。 這看起來可能並不麻煩,似乎以下的代碼就可以滿足需求

public class Stats<T extends Number> {

    private T[] numbers;

    public Stats(T[] numbers) {
        this.numbers = numbers;
    }

    public double average() {
        double sum = 0;
        for (T t : numbers) {
            sum += t.doubleValue();
        }
        return sum / numbers.length;
    }

    public boolean sameAvg(Stats<T> ob) {
        return average() == ob.average();
    }

}

但在使用的過程中,你就會發現 sameAvg 方法給我們帶來了極大的限制,因爲 sameAvg() 方法的參數值泛型類型被指定爲和被調用對象的參數類型是相同的

如下的調用方式會使編譯器提示錯誤。因此,這種方式的適用範圍很窄,無法得到泛型化的解決方案。

image

爲了創建泛型化的 sameAvg() 方法,必須使用泛型的另一個特性:通配符參數。通配符參數由 “?” 指定,表示未知類型。在此,Stats< ? > 和所有的 Stats 對象相匹配,允許任意兩個Stats對象比較它們的平均值。通配符只是簡單地匹配所有有效的Stats對象

    public boolean sameAvg(Stats<?> ob) {
        return average() == ob.average();
    }

5.2、有界通配符

如果要爲通配符建立上邊界,可以使用如下所示的通配符表達式:

 <? extends superClass>

其中,superClass 是作爲上界的類的名稱。這是一條包含語句,即形成上界的類(即superClass)也包含於邊界之內

此外還可以爲通配符添加一條super子句,爲通配符指定下界

<? super subClass>

其中,只有 subClass 的超類是可接受參數。這是一條排除子句,即 subClass 指定的類不包含在內

六、創建泛型方法

泛型類是在實例化類的時候指明參數的具體類型,而泛型方法是在調用方法的時候指明參數的具體類型 。 創建泛型方法需要依照一定的規則:可以在非泛型類中創建泛型方法,類型參數列表要位於返回類型之前 類似於

    public <T> void mothodName(T t) {
        
    }

在泛型類中聲明的一般方法與泛型方法是有一些概念與使用區別的

public class Stats<T> {

    private T[] numbers;

    public Stats(T[] numbers) {
        this.numbers = numbers;
    }

    //這不是泛型方法,只是使用了 T 作爲泛型參數而已
    public double average(T t) {
        return 1;
    }

    //這是一個泛型參數,
    //在泛型類 Stats<T> 中聲明瞭一個泛型方法,使用泛型 K,泛型 K 可以爲任意類型,既可以與 T 相同,也可以不同
    //由於泛型方法聲明瞭泛型參數<K>,因此即使在泛型類 Stats<T> 中並未聲明 K,編譯器也能夠正確識別出泛型方法中使用到的參數類型
    public <K> void mothodName(K t) {

    }

}

七、使用泛型的限制

7.1、模糊性錯誤

看如下例子 對泛型類 User< T, K > 而言,聲明瞭兩個泛型類參數:T 和 K。在類中試圖根據類型參數的不同重載 set() 方法。這看起來沒什麼問題,可編譯器會報錯

public class User<T, K> {
    
    //重載錯誤
    public void set(T t) {
        
    }

    //重載錯誤
    public void set(K k) {

    }

}

首先,當聲明 User 對象時,T 和 K 實際上不需要一定是不同的類型,以下的兩種寫法都是正確的

public class GenericMain {

    public static void main(String[] args) {
        User<String, Integer> stringIntegerUser = new User<>();
        User<String, String> stringStringUser = new User<>();
    }

}

對於第二種情況,T 和 K 都將被 String 替換,這使得 set() 方法的兩個版本完全相同,所以會導致重載失敗。

此外,對 set() 方法的類型擦除會使兩個版本都變爲如下形式:

    public void set(Object o) {
        
    }

一樣會導致重載失敗。

7.2、不能實例化類型參數

不能創建類型參數的實例。因爲編譯器不知道創建哪種類型的對象,T 只是一個佔位符

public class User<T> {

    private T t;

    public User() {
        //錯誤
        t = new T();
    }


}

7.3、對靜態成員的限制

靜態成員不能使用在類中聲明的類型參數,但是可以聲明靜態的泛型方法

public class User<T> {

    //錯誤
    private static T t;

    //錯誤
    public static T getT() {
        return t;
    }

    //正確
    public static <K> void test(K k) {

    }

}

7.4、對泛型數組的限制

不能實例化元素類型爲類型參數的數組,但是可以將數組指向類型兼容的數組的引用

public class User<T> {

    private T[] values;

    public User(T[] values) {
        //錯誤,不能實例化元素類型爲類型參數的數組
        this.values = new T[5];
        //正確,可以將values 指向類型兼容的數組的引用
        this.values = values;
    }

}

此外,不能創建特定類型的泛型引用數組,但使用通配符的話可以創建指向泛型類型的引用的數組

public class User<T> {

    private T[] values;

    public User(T[] values) {
        this.values = values;
    }

}
public class GenericMain {

    public static void main(String[] args) {
        //錯誤,不能創建特定類型的泛型引用數組
        User<String>[] stringUsers = new User<>[10];
        //正確,使用通配符的話,可以創建指向泛型類型的引用的數組
        User<?>[] users = new User<?>[10];
    }

}

7.5、對泛型異常的限制

泛型類不能擴展 Throwable,意味着不能創建泛型異常類

我的GitHub主頁: leavesC

我的簡書主頁: 葉應是葉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章