主頁 > 後端開發 > 從一知半解到揭曉Java高級語法—泛型

從一知半解到揭曉Java高級語法—泛型

2020-09-10 18:11:15 後端開發

目錄

  • 前言
    • 探討
    • 泛型解決了什么問題?
    • 擴展
  • 引入泛型
    • 什么是泛型?
    • 泛型類
    • 泛型介面
    • 泛型方法
    • 型別擦除
    • 擦除的問題
  • 邊界
  • 通配符
    • 上界通配符
    • 下界通配符
    • 通配符和向上轉型
  • 泛型約束
  • 實踐總結
    • 泛型命名
    • 使用泛型的建議
  • 參考資料:

前言

泛型是Java基礎知識的重點,雖然我們在初學Java的時候,都學過泛型,覺得自己掌握對于Java泛型的使用(全是錯覺),往后的日子,當我們深入去閱讀一些框架原始碼,你就發現了,自己會的只是簡單的使用,卻看不懂別人的泛型代碼是怎么寫的,還可以這樣,沒錯,別人寫出來的代碼那叫藝術,而我......

探討

Java語言為什么存在著泛型,而像一些動態語言Python,JavaScipt卻沒有泛型的概念?

原因是,像JavaC#這樣的靜態編譯型的語言,它們在傳遞引數的時候,引數的型別,必須是明確的,看一個例子,簡單撰寫一個存放int型別的堆疊—StackInt,代碼如下:

public class StackInt {

    private int maxSize;
    private int[] items;
    private int top;

    public StackInt(int maxSize){
        this.maxSize = maxSize;
        this.items = new int[maxSize];
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(int value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public int pop(){
        if(this.isNull()){
            throw new RuntimeException("當前堆疊中無資料");
        }
        int value = https://www.cnblogs.com/kalton/p/this.items[top];
        --top;
        return value;
    }
}

在這里使用建構式初始化一個StackInt物件時,可以傳入String字串嗎?很明顯是不行的,我們要求的是int型別,傳入字串String型別,這樣在語法檢查階段時會報錯的,像Java這樣的靜態編譯型的語言,引數的型別要求是明確的

在這里插入圖片描述

泛型解決了什么問題?

引數不安全:引入泛型,能夠在編譯階段找出代碼的問題,而不是在運行階段

泛型要求在宣告時指定實際資料型別,Java 編譯器在編譯時會對泛型代碼做強型別檢查,并在代碼違反型別安全時發出告警,早發現,早治理,把隱患扼殺于搖籃,在編譯時發現并修復錯誤所付出的代價遠比在運行時小,

避免型別轉換:

未使用泛型:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);    //需要在取出Value的時候進行強制轉換

使用泛型:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   //不需要強制轉換

重復編碼::通過使用泛型,可以實作通用編碼,可以處理不同型別的集合,并且型別安全且易于閱讀,像上面的StackInt類,我們不能針對每個型別去撰寫對應型別的堆疊,那樣太麻煩了,而泛型的出現就很好的解決了這點

擴展

在上面的StackInt類有一些不好的地方,那就是太具體了,不夠抽象,不夠抽象,那么它的復用性也是不高的,例如,在另外的場景下,我需要的是往堆疊里存String型別的字串,或者是其他型別,那么StackInt類就做不到了,那么有什么方法能夠做到呢?再寫一個StackString類,不可能,那樣不得累死,那就只有引入基類Object了,我們改進一下代碼:

public class StackObject {

    private int maxSize;
    private Object[] items;
    private int top;

    public StackObject(int maxSize){
        this.maxSize = maxSize;
        this.items = new Object[maxSize];
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(Object value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public Object pop(){
        if(this.isNull()){
            throw new RuntimeException("當前堆疊中無資料");
        }
        Object value = https://www.cnblogs.com/kalton/p/this.items[top];
        --top;
        return value;
    }
}

使用StackObject可以存盤任意型別的資料,那么這樣做,又有什么優點和缺點呢?

優點:StackObject類變得相對抽象了,我們可以往里面存盤任何型別的資料,這樣就避免了寫一些重復代碼
在這里插入圖片描述

缺點:

1、用Object表示的物件是比較抽象的,它失去了型別的特點,那么我們在做一些運算的時候,可能會頻繁的拆箱裝箱的程序

在這里插入圖片描述

看上面的例圖,我們理解的認為存放了兩個數值,1234554321,將兩個進行相加,這是很常見的操作,但是報錯了,編譯器給我們的提示是,+操作運算不能用于兩個Object型別,那么只能對其進行型別轉換,這也是我們上面說到的泛型能解決的問題,我們需要這樣做,int sum = (int)val1 + (int)val2;,同時在涉及拆箱裝箱時,是有一定性能的損耗的,關于拆箱裝箱在這里不作描述,可以參考我寫過的隨筆—— 深入理解Java之裝箱與拆箱

2、對于我們push進去的值,我們在取出的時候,容易忘記型別轉換,或者不記得它的型別,型別轉換錯誤,這在后面的一些業務可能埋下禍根,例如下面這個場景:直到運行時錯誤才暴露出來,這是不安全的,也是違反軟體開發原則的,應該盡早的在編譯階段就發現問題,解決問題

在這里插入圖片描述

3、使用Object太過于模糊了,沒有具體型別的意義

最好不要用到Object,因為Object是一切型別的基類,也就是說他把一些型別的特點給抹除了,比如上面存的數字,對于數字來說,加法運算就是它的一個特點,但是用了Object,它就失去了這一特點,失去型別特有的行為

引入泛型

什么是泛型?

泛型:是被引數化的類或介面,是對型別的約定

泛型類

class name<T1, T2, ..., Tn> { /* ... */ }

一般將泛型中的類名稱為原型,而將 <> 指定的引數稱為型別引數<> 相當于型別的約定,T就是型別,相當于一個占位符,由我們在呼叫時指定

使用泛型改進一下上面StackObject類,但是,陣列和泛型不能很好地結合,你不能實體化具有引數化型別的陣列,例如下面的代碼是不合格的:

public StackT(int maxSize){
    this.maxSize = maxSize;
    this.items = new T[maxSize];
    this.top = -1;
}

在這里插入圖片描述

Java 中不允許直接創建泛型陣列,這是因為相比于C++,C#的語法,Java泛型其實是偽泛型,這點在后面會說到,但是,可以通過創建一個型別擦除的陣列,然后轉型的方式來創建泛型陣列,

private int maxSize;
private T[] items;     
private int top;

public StackT(int maxSize){
    this.maxSize = maxSize;
    this.items = (T[]) new Object[maxSize];
    this.top = -1;
}

實際上,真的需要存盤泛型,還是使用容器更合適,回到原來的代碼上,需要知道的是,泛型型別不能是基本型別的,需要是包裝類

在這里插入圖片描述

上面說到了Java 中不允許直接創建泛型陣列,事實上,Java中的泛型我們是很難通new的方式去實體化物件,不僅僅是實體化物件,甚至是獲取T的真實型別也是很難的,當然通過反射的機制還是可以獲取到的,Java獲取真實型別的方式有 3 種,分別是:

1、類名.class

2、物件.getClass

3、class.forName("全限定類名")

但是,在這里,12的方式都是做不到的,雖然我們在外邊明確的傳入了Integer型別,new StackT<Integer>(3);但是在StackT

類,使用T.class還是獲取不到真實型別的,第 2 種方式的話,并沒有傳入物件,前面也說到是沒有辦法new方式實體化的,而通過反射機制是可以做到的,這里不作演示,需要了解的話可以參考 —— Java如何獲得泛型類的真實型別、 Java通過反射獲取泛型的型別

在這里插入圖片描述

但是在C#中的泛型以及C++的模板,這是很容易做到的,所以說Java的泛型是偽泛型,Java并不是做不到像C#一樣,而是為了遷就老的JDK語法所作出的妥協,至于上面為什么做不到這樣,這就要說到泛型的型別擦除了,

再說型別擦除之前,先說一下泛型介面,和泛型方法吧

泛型介面

介面也可以宣告泛型,泛型介面語法形式:

public interface Content<T> {
    T text();
}

泛型介面有兩種實作方式:

  • 實作介面的子類明確宣告泛型型別
public class ContentImpl implements Content<Integer> {
    private int text;

    public ContentImpl(int text) {
        this.text = text;
    }

    public static void main(String[] args) {
        ContentImpl one = new ContentImpl(10);
        System.out.print(one.text());
    }
}
// Output:
// 10
  • 實作介面的子類不明確宣告泛型型別
public class ContentImpl<T> implements Content<T> {
    private T text;

    public ContentImpl(T text) {
        this.text = text;
    }

    @Override
    public T text() { return text; }

    public static void main(String[] args) {
        ContentImpl<String> two = new ContentImpl<>("ABC");
        System.out.print(two.text());
    }
}
// Output:
// ABC

泛型方法

泛型方法是引入其自己的型別引數的方法,泛型方法可以是普通方法、靜態方法以及構造方法,

泛型方法語法形式如下:

public <T> T func(T obj) {}

是否擁有泛型方法,與其所在的類是否是泛型沒有關系,

泛型方法的語法包括一個型別引數串列,在尖括號內,它出現在方法的回傳型別之前,對于靜態泛型方法,型別引數部分必須出現在方法的回傳型別之前,型別引數能被用來宣告回傳值型別,并且能作為泛型方法得到的實際型別引數的占位符,

使用泛型方法的時候,通常不必指明型別引數,因為編譯器會為我們找出具體的型別,這稱為型別引數推斷(type argument inference),型別推斷只對賦值操作有效,其他時候并不起作用,如果將一個泛型方法呼叫的結果作為引數,傳遞給另一個方法,這時編譯器并不會執行推斷,編譯器會認為:呼叫泛型方法后,其回傳值被賦給一個 Object 型別的變數,

public class GenericsMethod {
    public static <T> void printClass(T obj) {
        System.out.println(obj.getClass().toString());
    }

    public static void main(String[] args) {
        printClass("abc");
        printClass(10);
    }
}
// Output:
// class java.lang.String
// class java.lang.Integer

泛型方法中也可以使用可變引數串列

public class GenericVarargsMethod {
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        Collections.addAll(result, args);
        return result;
    }

    public static void main(String[] args) {
        List<String> ls = makeList("A");
        System.out.println(ls);
        ls = makeList("A", "B", "C");
        System.out.println(ls);
    }
}
// Output:
// [A]
// [A, B, C]

型別擦除

事實上,Java的運行大致可以分為兩個階段,編譯階段運行階段

那么對于Java泛型來說,當編譯階段過后,泛型 T 是已經被擦除了,所以在運行階段,它已經丟失了 T 的具體資訊,而我們去實體化一個物件的時候,比如T c = new T();,它的發生時機是在運行階段,而在運行階段,你要new T(),就需要知道 T 的具體型別,實際上這時候 T是被替換成Integer了,而JVM是不知道T的型別的,所以是沒有辦法實體化的,

那么,型別擦除做了什么呢?它做了以下作業:

  • 把泛型中的所有型別引數替換為 Object,如果指定型別邊界,則使用型別邊界來替換,因此,生成的位元組碼僅包含普通的類,介面和方法,
  • 擦除出現的型別宣告,即去掉 <> 的內容,比如 T get() 方法宣告就變成了 Object get()List<String> 就變成了 List,如有必要,插入型別轉換以保持型別安全,
  • 生成橋接方法以保留擴展泛型型別中的多型性,型別擦除確保不為引數化型別創建新類;因此,泛型不會產生運行時開銷,

讓我們來看一個示例:

import java.util.*;

public class ErasedTypeEquivalence {

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }

}
/* Output:
true
*/

ArrayList<String>ArrayList<Integer> 應該是不同的型別,不同的型別會有不同的行為,例如,如果嘗試向 ArrayList<String> 中放入一個 Integer,所得到的行為(失敗)和 向 ArrayList<Integer> 中放入一個 Integer 所得到的行為(成功)完全不同,但是結果輸出的是true,這意味著使用泛型時,任何具體的型別資訊都被擦除了,ArrayList<Object>ArrayList<Integer> 在運行時,JVM 將它們視為同一型別class java.util.ArrayList

再用一個例子來對于該謎題的補充:

import java.util.*;

class Frob {}
class Fnorkle {}
class Quark<Q> {}

class Particle<POSITION, MOMENTUM> {}

public class LostInformation {

    public static void main(String[] args) {
        
        List<Frob> list = new ArrayList<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long, Double> p = new Particle<>();
        
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
    }

}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/

根據 JDK 檔案,Class.getTypeParameters() “回傳一個 TypeVariable 物件陣列,表示泛型宣告中宣告的型別引數...” 這暗示你可以發現這些引數型別,但是正如上例中輸出所示,你只能看到用作引數占位符的識別符號,這并非有用的資訊,

殘酷的現實是:在泛型代碼內部,無法獲取任何有關泛型引數型別的資訊,

以上兩個例子皆出《Java 編程思想》第五版 —— On Java 8中的例子,本文借助該例子,試圖講清楚Java泛型是使用型別擦除這里機制實作的,能力不足,有錯誤的地方,還請指正,關于On Java 8一書,已在github上開源,并有熱心的伙伴將之翻譯成中文,現在給出閱讀地址,On Java 8

擦除的問題

擦除的代價是顯著的,泛型不能用于顯式地參考運行時型別的操作中,例如轉型、instanceof 操作和 new 運算式,因為所有關于引數的型別資訊都丟失了,當你在撰寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有有關引數的型別資訊而已,

考慮如下的代碼段:

class Foo<T> {
    T var;
}

看上去當你創建一個 Foo 實體時:

Foo<Cat> f = new Foo<>();

class Foo 中的代碼應該知道現在作業于 Cat 之上,泛型語法也在強烈暗示整個類中所有 T 出現的地方都被替換,就像在 C++ 中一樣,但是事實并非如此,當你在撰寫這個類的代碼時,必須提醒自己:“不,這只是一個 Object“,

繼承問題

泛型時基于型別擦除實作的,所以,泛型型別無法向上轉型

向上轉型是指用子類實體去初始化父類,這是面向物件中多型的重要表現,

在這里插入圖片描述

Integer 繼承了 ObjectArrayList 繼承了 List;但是 List<Interger> 卻并非繼承了 List<Object>

這是因為,泛型類并沒有自己獨有的 Class 類物件,比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 編譯器會將二者都視為 List.class

如何解決上面所產生的問題:

其實并不一定要通過new的方式去實體化,我們可以通過顯式的傳入源類,一個Class<T> clazz的物件來補償擦除,例如instanceof 操作,在程式中嘗試使用 instanceof 將會失敗,型別標簽可以使用動態 isInstance() ,這樣改進代碼:

public class Improve<T> {
	
    //錯誤方法
    public boolean  f(Object arg) {
        // error: illegal generic type for instanceof
        if (arg instanceof T) {
            return true;
        }
        return false;
    }
    //改進方法
    Class<T> clazz;
    
	public Improve(Class<T> clazz) {
        this.clazz = clazz;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
}

實體化:

試圖在 new T() 是行不通的,部分原因是由于擦除,部分原因是編譯器無法驗證 T 是否具有默認(無參)建構式,

Java 中的解決方案是傳入一個工廠物件,并使用該物件創建新實體,方便的工廠物件只是 Class 物件,因此,如果使用型別標記,則可以使用 newInstance() 創建該型別的新物件:

class Improve<T> {
    Class<T> kind;

    Improve(Class<T> kind) {
        this.kind = kind;
    }
    
    public T get(){
        try {
            return kind.newInstance();
        } catch (InstantiationException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee {
    @Override
    public String toString() {
        return "Employee";
    }
}

public class InstantiateGenericType {
    public static void main(String[] args) {
        Improve<Employee> fe = new Improve<>(Employee.class);
        System.out.println(fe.get());
    }
}
/* Output:
Employee
*/

通過這樣改進代碼,可以實作創建物件的實體,但是要注意的是,newInstance();方法呼叫無參建構式的,如果傳入的型別,沒有無參構造的話,是會拋出InstantiationException例外的,

泛型陣列

泛型陣列這部分,我們在上面說到可以通過創建一個型別擦除的陣列,然后轉型的方式來創建泛型陣列,這次我們可以通過顯式的傳入源類的方式來撰寫StackT類,解決創建泛型陣列的問題,代碼如下:

public class StackT<T> {

    private int maxSize;
    private T[] items;
    private int top;

    public StackT(int maxSize, Class<T> clazz){
        this.maxSize = maxSize;
        this.items = this.createArray(clazz);
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(T value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public T pop(){
        if(this.isNull()){
            throw new RuntimeException("當前堆疊中無資料");
        }
        T value = https://www.cnblogs.com/kalton/p/this.items[top];
        --top;
        return value;
    }

    private T[] createArray(Class clazz){
        T[] array =(T[])Array.newInstance(clazz, this.maxSize);
        return array;
    }

}

邊界

有時您可能希望限制可在引數化型別中用作型別引數的型別,型別邊界可以對泛型的型別引數設定限制條件,例如,對數字進行操作的方法可能只想接受 Number 或其子類的實體,

要宣告有界型別引數,請列出型別引數的名稱,然后是 extends 關鍵字,后跟其限制類或介面,

型別邊界的語法形式如下:

<T extends XXX>

示例:

public class GenericsExtendsDemo01 {
    static <T extends Comparable<T>> T max(T x, T y, T z) {
        T max = x; // 假設x是初始最大值
        if (y.compareTo(max) > 0) {
            max = y; //y 更大
        }
        if (z.compareTo(max) > 0) {
            max = z; // 現在 z 更大
        }
        return max; // 回傳最大物件
    }

    public static void main(String[] args) {
        System.out.println(max(3, 4, 5));
        System.out.println(max(6.6, 8.8, 7.7));
        System.out.println(max("pear", "apple", "orange"));
    }
}
// Output:
// 5
// 8.8
// pear

示例說明:

上面的示例宣告了一個泛型方法,型別引數 T extends Comparable<T> 表明傳入方法中的型別必須實作了 Comparable 介面,

型別邊界可以設定多個,語法形式如下:

<T extends B1 & B2 & B3>

注意:extends 關鍵字后面的第一個型別引數可以是類或介面,其他型別引數只能是介面,

通配符

通配符是Java泛型中的一個非常重要的知識點,很多時候,我們其實不是很理解通配符和泛型型別T區別,容易混淆在一起,其實還是很好理解的,T 都表示不確定的型別,區別在于我們可以對 T 進行操作,但是對 不行,比如如下這種 :

// 可以
T t = operate();
// 不可以
? car = operate();

但是這個并不是我們混淆的原因,雖然T 都表示不確定的型別,T 通常用于泛型類和泛型方法的定義,通常用于泛型方法的呼叫代碼和形參,不能用于定義類和泛型方法,用代碼解釋一下,回到文章最初說的堆疊類StackT,我們以這個為基礎來解釋,上面的觀點:

public class Why {
    public static void main(String[] args) {

        StackT<Integer> stackT = new StackT<>(3, Integer.class);
        stackT.push(8);
        StackT<String> stackT1 = new StackT<>(3, String.class);
        stackT1.push("7");
        test(stackT1);

    }
    public static void test(StackT stackT){
        System.out.println(stackT.pop());
    }
}
// Output: 8

以我們撰寫的StackT類,進行測驗,撰寫一個test方法,傳入引數型別StackT,上面的程式正常輸出字串"7" ,這沒有什么問題,問題在這里失去了泛型的限定,傳進去的實參StackT1,是被我們限定為StackT<String> ,但是我們通過編譯器可以看到stackT.pop()出來的物件,并沒有String型別的特有方法,也就是說,它其實是Object

在這里插入圖片描述

那么我們就需要修改test方法的形參,改為:

public static void test(StackT<String> stackT){
    System.out.println(stackT.pop());
}

這樣子就回到了我們問題的本質來了,將形參修改為StackT<String>,這起到了泛型的限定作用,但是會出現這樣的問題,如果我們需要向該方法傳入StackT<Integer>型別的物件 stackT是,因為方法形參限定了StackT<String>,,這時候就報錯了

在這里插入圖片描述

這個時候就是通配符?起作用了,將方法形參改為StackT<?>就可以了,這也就確定了我們剛剛的結論,通配符通常是用于泛型傳參,而不是泛型類的定義,

public static void test(StackT<?> stackT){
    System.out.println(stackT.pop());
}

但是這種用法我們通常也不會去用,因為它還是失去了型別的特點,即當無界泛型通配符作為形參時,作為呼叫方,并不限定傳遞的實際引數型別,但是,在方法內部,泛型類的引數和回傳值為泛型的方法,不能使用!

在這里插入圖片描述

這里,StackT.push就不能用了,因為我并不知道?傳的是Integer還是String ,還是其他型別,所以是會報錯的,

但是我們有時候是有這樣的需求的,我們在接收泛型堆疊StackT作為形參的時候,我想表達一種約束的關系,但是又不像StackT<String>一樣,約束的比較死板,而Java是面向物件的語言,那么就會有繼承的機制,我想要的約束關系是我能接收的泛型堆疊的型別都是Number類的派生類,即不會像?無界通配符一樣失去類的特征,又不會像StackT<String>約束的很死,這就引出了上界通配符的概念,

上界通配符

可以使用上界通配符來縮小型別引數的型別范圍,

它的語法形式為:<? extends Number>

public class Why {
    public static void main(String[] args) {

        StackT<Integer> stackT = new StackT<>(3, Integer.class);
        stackT.push(8);
        StackT<String> stackT1 = new StackT<>(3, String.class);
        stackT1.push("7");
        StackT<Double> stackT2 = new StackT<>(3, Double.class);
        
        //通過
        test(stackT);
        test(stackT2);
        //error
        test(stackT1);

    }
    
    public static void test(StackT<? extends Number> stackT){

        System.out.println(stackT.pop());
    }
}

這樣就實作了一型別別的限定,但是需求變更了,我現在希望的約束關系是我能接收的泛型堆疊的型別都是Number類的父類,或者父類的父類,那么有上界,自然就有下界

下界通配符

下界通配符將未知型別限制為該型別的特定型別或超型別別,

注意:上界通配符和下界通配符不能同時使用

它的語法形式為:<? super Number>

public class Why {
    public static void main(String[] args) {

        StackT<Number> stackT1 = new StackT<>(3, Number.class);
        stackT1.push(8);
        StackT<Double> stackT2 = new StackT<>(3, Double.class);
        StackT<Object> stackT3 = new StackT<>(3, Object.class);
        //通過
        test(stackT1);
        test(stackT3);
        //error
        test(stackT2);

    }

    public static void test(StackT<? super Number> stackT){

        System.out.println(stackT.pop());
    }
}

這樣子的話,就確保了我們的test方法只接收Number型別以上的方法,泛型的各種高級語法可能在寫業務代碼的時候可以規避,但是如果你要去寫一些框架的時候,由于你不知道框架的使用者的使用場景,那么掌握泛型的高級語法就很有用了,

通配符和向上轉型

前面,我們提到:泛型不能向上轉型,但是,我們可以通過使用通配符來向上轉型

public class GenericsWildcardDemo {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numList = intList;  // Error

        List<? extends Integer> intList2 = new ArrayList<>();
        List<? extends Number> numList2 = intList2;  // OK
    }
}

通配符邊界問題,關于一些更加深入的解惑可以參考整理的轉載的文章——Java泛型解惑之上下通配符

泛型約束

  • 泛型型別的型別引數不能是值型別
Pair<int, char> p = new Pair<>(8, 'a');  // 編譯錯誤
  • 不能創建型別引數的實體
public static <E> void append(List<E> list) {
    E elem = new E();  // 編譯錯誤
    list.add(elem);
}
  • 不能宣告型別為型別引數的靜態成員
public class MobileDevice<T> {
    private static T os; // error

    // ...
}
  • 型別引數不能使用型別轉換或 instanceof
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // 編譯錯誤
        // ...
    }
}
List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // 編譯錯誤
  • 不能創建型別引數的陣列
List<Integer>[] arrayOfLists = new List<Integer>[2];  // 編譯錯誤
  • 不能創建、catch 或 throw 引數化型別物件
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // 編譯錯誤

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯誤
public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}
  • 僅僅是泛型類相同,而型別引數不同的方法不能多載
public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { } // 編譯錯誤
}

實踐總結

泛型命名

泛型一些約定俗成的命名:

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

使用泛型的建議

  • 消除型別檢查告警
  • List 優先于陣列
  • 優先考慮使用泛型來提高代碼通用性
  • 優先考慮泛型方法來限定泛型的范圍
  • 利用有限制通配符來提升 API 的靈活性
  • 優先考慮型別安全的異構容器

參考資料:

深入理解 Java 泛型

On Java 8

Java泛型解惑之 extends T>和 super T>上下界限

7月的直播課——Java 高級語法—泛型

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/3020.html

標籤:Java

上一篇:uni-app搜索功能前后端開發(頁面)

下一篇:2020年小米高級 PHP 工程師面試題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more