1、概述

泛型的概念:

泛型讓類、接口或方法在定義時不指定具體類型,而在使用時再確定類型。
換句話説:泛型讓代碼在“寫的時候”更通用,在“用的時候”更安全。

泛型的好處:類型安全、消除強制類型轉換

2、泛型類

2.1 泛型類的定義

●  泛型類的定義語法:

class 類名稱<泛型標識, 泛型標識, ...> {
    private 泛型標識 變量名;
    .....
}

● 常用的泛型標識:T、E、K、V...

泛型類定義舉例:

package com.itheima.demo2;

/**
 * 泛型類的定義
 * @param <T> 泛型標識——類型形參
 *            T 創建對象的時候指定具體的類型
 */
public class Generic<T> {
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

2.2 泛型類的使用

● Java1.7以後,後面的<>中的具體的數據類型可以省略不寫

類名<具體的數據類型> 對象名 = new 類名<>();

泛型類的使用舉例:

Generic<String> generic = new Generic<>("a");

 2.3 泛型類的注意事項

①泛型類在創建對象的時候,沒有指定類型,將按照Object類型來操作。

Generic generic = new Generic("ABC");
Object key3 = generic.getKey();
System.out.println("key3:" + key3);

②泛型類,不支持基本數據類型。

Generic<int> generic1 = new Generic<int>(100);

③泛型類雖然指定的類型是不同的,但本質上是同一個類型

System.out.println(intGeneric.getClass());
System.out.println(strGeneric.getClass());//他倆輸出結果是一樣的

2.4 從泛型類派生的子類

子類也是泛型類的情況下,子類和父類的泛型類型要一致

class ChildGeneric<T> extends Generic<T>

子類不是泛型類的情況下,父類要明確泛型的數據類型

class ChildGeneric extends Generic<String>

舉例説明  子類是泛型類:

//這是一個父類  是泛型類

public class Parent<E> {
    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
}

//若此時定義了一個子類,要繼承父類(標識為T),那它的泛型標識也一定是T,要不然報錯
public class ChildFirst<T> extends Parent<T> {

    @Override
    public T getValue() {
        return super.getValue();
    }
}

舉例説明子類不是泛型類

//子類不是泛型類,父類是泛型類的話,不能寫泛型類型,必須指定父類數據類型,否則報錯
public class ChildSecond extends Parent<Integer> {
    @Override
    public Integer getValue() {
        return super.getValue();
    }

    @Override
    public void setValue(Integer value)

3、泛型接口

3.1 泛型接口的定義

● 泛型接口的定義語法

interface 接口名稱<泛型標識, 泛型標識, ...> {
    泛型標識 方法名();
    .....
}

3.2 泛型接口的使用

和前面的泛型類很類似

實現類不是泛型類的情況下,接口要明確數據類型

//這是一個泛型接口
public interface Generator<T> {
    T getKey();
}
//這不是一個泛型類,實現了泛型接口
public class Apple implements Generator<String> {
    @Override
    public String getKey() {
        return "hello generic";
    }
}

實現類也是泛型類的情況下,實現類和接口的泛型類型要一致

//這是一個泛型接口
public interface Generator<T> {
    T getKey();
}
//這是一個泛型類,實現了泛型接口
public class Pair<T> implements Generator<T> {
    @Override
    public T getKey() {
        return null;
    }
}

4、泛型方法

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

你可能會問,剛剛講泛型類的時候不是已經包含了泛型方法嗎?

那個其實是屬於泛型類的成員方法,不是泛型方法!

泛型方法是我有泛型列表的,而泛型類的成員方法只在參數列表中有泛型標識

4.1 泛型方法

語法

修飾符 <T, E, ...> 返回值類型 方法名(形參列表) {
    方法體...
}

public 與返回值中間的 <T> 非常重要,可以理解為聲明此方法為泛型方法。只有聲明瞭 <T> 的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。

舉個例子

public class GenericMethodExample {

    // 泛型方法:交換任意類型數組中兩個元素的位置
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

<T>就是泛型列表,它告訴編譯器:“我這個方法裏有一個類型參數 T,可以在方法體中使用”。

後面的 T[] array使用這個類型參數

我們也可以在泛型類中使用泛型方法,泛型方法的泛型標識獨立於泛型類(即便是相同標識)

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    // 泛型方法(與類的泛型參數 T 無關)
    public <E> void print(E element) {
        System.out.println("打印內容:" + element);
    }
}

這個方法在 void 前面又聲明瞭一個新的類型參數 <E>;它與類的泛型參數 T 沒有任何關係

請注意泛型類中的方法不能是 static,如果它要用類的泛型參數

但是泛型方法可以是 static的泛型方法的類型參數是方法級別的,不依賴於類的泛型參數。)

4.2 泛型方法與可變參數

語法

public <E> void print(E... e) {
    for (E el : e) {
        System.out.println(e);
    }
}

舉個例子

public static <E> void print(E... e) {
    for (int i = 0; i < e.length; i++) {
        System.out.println(e[i]);
    }
}
Box<Integer> intBox = new Box<>();
Box<String> strBox = new Box<>();

4.3 泛型方法的總結

  • 泛型方法能使方法獨立於類而產生變化
  • 如果 static 方法要使用泛型能力,就必須使其成為泛型方法

5、類型通配符

  • 類型通配符一般是使用“?”

我來細細講講,這裏很繞

這個T,是在定義過程中寫的,僅僅是個佔位符,等用户來填

class Box<T> { // T 是類型形參
    private T value;
    public T getFirst() { return value; }
    public void setFirst(T value) { this.value = value; }
}

當別人使用時, IntegerString 代替了 T

Box<Integer> intBox = new Box<>();
Box<String> strBox = new Box<>();

這個< ?>是使用過程中寫的

public static void showBox(Box<?> box)

這個< ?>彷彿在説“我接收一個 Box,但是我不知道它裝的是什麼類型”,? 是“實參中的未知者”。

5.1 類型通配符的使用案例

①我們定義了一個泛型類  叫Box

public class Box<E> {
    private E first;

    public E getFirst() {
        return first;
    }

    public void setFirst(E first) {
        this.first = first;
    }
}

②又定義了一個靜態方法,用於展示box對象的屬性

Number是數值類型的共同父類(抽象類)。

public static void showBox(Box<Number> box) {
    Number first = box.getFirst();
    System.out.println(first);
}

在這裏,請注意!!

Box<Number>它可以任何 Number 及其子類的對象(比如 IntegerDouble)。

所以:我寫如下代碼沒問題

Box<Number> box1 = new Box<>();
    box1.setFirst(100);
    showBox(box1);

但是寫如下代碼會出問題

Box<Integer> box2 = new Box<>();
    box2.setFirst(200);
    showBox(box2);//這裏會報錯

為什麼會報錯?

IntegerNumber 的子類

Box<Integer> 不是 Box<Number> 的子類!

你往showBox中傳遞的參數是 Box<Integer> 類型,但是showBox所需要的是Box<Number>類型!

那怎麼辦?用類型通配符

public static void showBox(Box<?> box) {
    Object first = box.getFirst();
    System.out.println(first);
}

5.2 類型通配符的上限

定義:

類/接口<? extends 實參類型>

根據我們剛剛的例子,可以傳遞Box<Integer> 也可以是Box<Number>類型,還可以是其他類型

但我們設置了類型通配符的上限後(比如上限是Number):

public static void showBox(Box<? extends Number> box) {
    Number first = box.getFirst();
    System.out.println(first);
}

代表着,我只可以接收Box的泛型是Number或其子類的對象

注意一點,我們不能在box中add元素,我舉例説明:

我們有一個簡單的繼承結構:

class Animal {}
class Cat extends Animal {}
class MiniCat extends Cat {}

然後我們準備三種容器:

List<Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
List<MiniCat> miniCats = new ArrayList<>();

定義一個方法,能接收裝各種“貓或貓的子類”的容器:

public static void readCats(List<? extends Cat> list) {
    Cat c = list.get(0); // ✅ 可以安全讀取為 Cat
    // list.add(new Cat());    // ❌ 不行
    // list.add(new MiniCat()); // ❌ 不行
    System.out.println(c.getClass().getSimpleName());
}

為什麼 add 不行?

假設你是這樣調用方法的:readCats(miniCats);

那麼 list 實際指向 List<MiniCat>。list中的元素必須是MiniCat類型和其子類,

如果我寫了add(new Cat()),那肯定不對,因為Cat不是MiniCat類型和其子類

所以編譯器為了安全,乾脆禁止任何 add(除了 null)。
但你可以 get,因為無論實際是哪種貓,它都至少是個 Cat

5.3 類型通配符的下限(1)

定義

類/接口<? super 實參類型>

注意一點,我們不能在box中get元素,但是add可以

 和上面一樣,我們有一個簡單的繼承結構:

class Animal {}
class Cat extends Animal {}
class MiniCat extends Cat {}

然後我們準備三種容器:

List<Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
List<MiniCat> miniCats = new ArrayList<>();

定義一個方法,能接收裝各種“貓或貓的父類”的容器:

public static void addCats(List<? super Cat> list) {
    list.add(new Cat());     // ✅ 可以
    list.add(new MiniCat()); // ✅ 也可以
    // Cat c = list.get(0);  // ❌ 不行,只能當 Object
    Object obj = list.get(0); // ✅ 只能讀成 Object
}

為什麼 add 可以?

假設傳入的是:addCats(animals);
list 實際是 List<Animal> ,所以list中必須是Animal或其子類,List<Animal> 里加 CatMiniCat,當然安全。

但如果反過來你想 Cat c = list.get(0),編譯器拒絕:
——因為它不知道你傳進來的是不是 List<Object>,如果是Object,你為什麼要用Cat接收? 安全起見,只能當作 Object 讀取。

5.4 類型通配符的下限(2)

這裏講了應用 以TreeSet為例,其中有兩個構造方法

Java泛型詳解,史上最全圖文詳解_#java

我們講第一個,傳構造器的

我們創建了三個類Animal、Cat、MiniCat

// 父類 Animal
public class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                '}';
    }
}
// 子類 Cat,繼承自 Animal
public class Cat extends Animal {
    public int age;

    public Cat(String name, int age) {
        super(name);
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
// 子類 MiniCat,繼承自 Cat
public class MiniCat extends Cat {
    public int level;

    public MiniCat(String name, int age, int level) {
        super(name, age);
        this.level = level;
    }

    @Override
    public String toString() {
        return "MiniCat{" +
                "level=" + level +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

又定義了三個比較器,用於TreeSet(一個類對應一個)

class Comparator1 implements Comparator<Animal> {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.name.compareTo(o2.name);
    }
}
class Comparator2 implements Comparator<Cat> {
    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.age - o2.age;
    }
}
class Comparator3 implements Comparator<MiniCat> {
    @Override
    public int compare(MiniCat o1, MiniCat o2) {
        return o1.level - o2.level;
    }
}

然後我們應用

public class Test08 {
    public static void main(String[] args) {
        
        TreeSet<Cat> treeSet = new TreeSet<>(new Comparator2());
        //TreeSet(Comparator<? super E> comparator),所以此時E是Cat
        //Comparator 必須滿足是Cat的父類



        treeSet.add(new Cat("jerry", 18));
        treeSet.add(new Cat("amy", 22));
        treeSet.add(new Cat("frank", 25));
        treeSet.add(new Cat("jim", 15));

        for (Cat cat : treeSet) {
            System.out.println(cat);
        }
    }
}

發現結果可以按照age排序

Java泛型詳解,史上最全圖文詳解_泛型_02

如果傳入父類Animal的比較器呢?

Java泛型詳解,史上最全圖文詳解_#java_03

發現也是可以的

如果傳入子類minicat的比較器呢

是不可以的!報錯!因為只可以傳Cat及其父類。從直覺上來説也容易理解,minicat多了一個屬性,TreeSet<cat>中又沒有這個屬性,沒法比較

6、類型擦除

 概念:

泛型是 Java 1.5 版本才引進的概念,在這之前是沒有泛型的,但是,泛型代碼能夠很好地和之前版本的代碼兼容。那是因為,泛型信息只存在於代碼編譯階段,在進入 JVM 之前,與泛型相關的信息會被擦除。我們稱之為——類型擦除。
 

舉個例子:

public static void main(String[] args) {
    ArrayList<Integer> intList = new ArrayList<>();
    ArrayList<String> strList = new ArrayList<>();

    System.out.println(intList.getClass().getSimpleName());
    System.out.println(strList.getClass().getSimpleName());
}

運行後發現

Java泛型詳解,史上最全圖文詳解_泛型_04

他們的類都是一樣的,所以這也就解釋了,為什麼我們編寫代碼的時候,看似類型不一樣,一個是ArrayList<Integer>、另外一個是 ArrayList<String>,但是運行後其實是歸屬於一個類型,因為Java 的泛型在編譯後被擦除

6.1 無限制類型擦除

Java泛型詳解,史上最全圖文詳解_泛型方法_05

什麼意思呢?當我的泛型標識為T的時候,在編譯後,T的類型會被擦除,變成Object

舉例説明:

我定義了一個泛型類

public class Erasure<T> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

寫了一個方法,輸出泛型類的屬性和類型

import java.lang.reflect.Field;

public class TestErasure {
    public static void main(String[] args) {
        // 創建泛型對象
        Erasure<Number> erasure = new Erasure<>();
        erasure.setKey(123);

        // 利用反射,獲取 Erasure 類的字節碼文件的 Class 對象
        Class<? extends Erasure> clz = erasure.getClass();

        // 獲取所有的成員變量
        Field[] declaredFields = clz.getDeclaredFields();

        // 打印成員變量的名稱和類型
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());
        }
    }
}

輸出:

Java泛型詳解,史上最全圖文詳解_泛型_06

所以,如果泛型類的泛型列表只有一個T,那類型擦除之後,確實是類型轉換成了Object

6.2 有限制類型擦除

Java泛型詳解,史上最全圖文詳解_泛型方法_07

什麼意思呢?當我的泛型標識為T entends Number的時候,在編譯後,T的類型會被擦除,變成Number

舉個例子

 我定義了一個泛型類

public class Erasure<T entends Number> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

寫了一個方法,輸出泛型類的屬性和類型

import java.lang.reflect.Field;

public class TestErasure {
    public static void main(String[] args) {
        // 創建泛型對象
        Erasure<Integer> erasure = new Erasure<>();
        erasure.setKey(123);

        // 利用反射,獲取 Erasure 類的字節碼文件的 Class 對象
        Class<? extends Erasure> clz = erasure.getClass();

        // 獲取所有的成員變量
        Field[] declaredFields = clz.getDeclaredFields();

        // 打印成員變量的名稱和類型
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName());
        }
    }
}

輸出為:

Java泛型詳解,史上最全圖文詳解_泛型方法_08

所以,如果泛型類的泛型列表是T extends Number,那類型擦除之後,確實是類型轉換成了Number

上面我説是泛型類,泛型方法也一樣!!

6.3 橋接方法

橋接方法存在於泛型接口中

Java泛型詳解,史上最全圖文詳解_泛型_09

可以看見,泛型標識被擦除成了Object,實現類沒有泛型標識,所以不變。除此之外,還產生了一個橋接方法,用於保持接口和類的實現關係

為什麼會有這個橋接方法?

擦除後,InfoImpl 這個類的 info(Integer) 方法不再匹配 接口裏的 info(Object) 方法簽名。

也就是説:
接口現在聲明的是 Object info(Object var)
而實現類提供的是 Integer info(Integer var)——
兩者在 JVM 層面是不同的簽名,
如果編譯器不處理,多態調用會失效

舉個例子

定義了一個接口

public interface Info<T> {
    T info(T t);
}

定義了接口的實現類

public class InfoImpl implements Info<Integer> {
    @Override
    public Integer info(Integer value) {
        return value;
    }
}

用反射,寫一個方法,獲取實現類的方法名、方法類型

Class<InfoImpl> infoClass = InfoImpl.class;

// 獲取所有的方法
Method[] infoImplMethods = infoClass.getDeclaredMethods();
for (Method method : infoImplMethods) {
    // 打印方法名稱和方法的返回值類型
    System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName());
}

運行結果如下

Java泛型詳解,史上最全圖文詳解_#開發語言_10

第二個info就是生成的橋接方法

7、泛型與數組

7.1 數組的協變

講正式內容之前,先講一下數組的協變

想象你有個動物園系統:

  • CatAnimal 的子類。
  • 一個 Animal[]數組 可以裝各種動物。

那你説:能不能用一個 Cat[] 去當作 Animal[] 使用?
Java 説:可以!

Cat[] cats = new Cat[3];//這是新創建了一個數組,數組引用是放在cats變量裏,指向的是實際的地址Cat[3]
Animal[] animals = cats; // ✅ 允許  將剛才的地址給animals,現在,animals和cats都存放着同樣的引用,指向實際的地址Cat[3]

這就是 協變 —— CatAnimal 的子類型,所以 Cat[] 也被當作 Animal[] 的子類型。

所以我可以往數組裏new一個cat對象

animals[0] = new Cat();  // cats[0] 也跟着變

由於數組是協變的,我也可以放其他Animal子類對象,於是我放一個dog對象

animals[1] = new Dog();

在編譯時期確實沒問題,編譯器會説:“沒毛病啊,Dog 是 Animal 嘛。”

但是在運行的時候出問題了,JVM 在運行時檢查到這塊堆內存的真實類型是 Cat[]

於是報錯!

ArrayStoreException: java.lang.Dog

JVM 拒絕往 Cat 數組裏塞一隻狗。

所以,數組是協變的,這本身就是不合理的,所以在開發中,即使是能用,也儘量少用或不用

7.2 泛型數組的創建

  • 可以聲明帶泛型的數組引用,但是不能直接創建帶泛型的數組對象

舉例:

ArrayList<String>[] listArr = new ArrayList<String>[5];//這樣是錯誤的

ArrayList<String>[] arr = new ArrayList[5];//這樣是正確的
  • 可以通過 java.lang.reflect.ArraynewInstance(Class<T>, int) 方法創建 T[] 數組

舉例:

我創建了一個操作數組的對象

public class Fruit<T> {
    private T[] array;

    public Fruit(Class<T> clz, int length) {
        // 通過 Array.newInstance 創建泛型數組
        array = (T[]) Array.newInstance(clz, length);
    }

    /**
     * 填充數組
     * @param index
     * @param item
     */
    public void put(int index, T item) {
        array[index] = item;
    }

    /**
     * 獲取數組元素
     * @param index
     * @return
     */
    public T get(int index) {
        return array[index];
    }

    public T[] getArray() {
        return array;
    }
}

就可以操作它

Fruit<String> fruit = new Fruit<>(String.class, 3);
fruit.put(0, "蘋果");
fruit.put(1, "西瓜");
fruit.put(2, "香蕉");

System.out.println(Arrays.toString(fruit.getArray()));

輸出:

Java泛型詳解,史上最全圖文詳解_泛型_11

7.3 泛型數組的陷阱(這裏沒看懂沒關係)

先看一個經典的泛型數組陷阱

public static void main(String[] args) {
    ArrayList[] list = new ArrayList[5];
    ArrayList<String>[] listArr = list;

    ArrayList<Integer> intList = new ArrayList<>();
    intList.add(100);

    list[0] = intList; // ⚠️ 這裏的賦值雖然能編譯,但留下了隱患

    String s = listArr[0].get(0); // 💥 運行時出錯
    System.out.println(s);
}

我講一下代碼執行的流程:

①ArrayList[] list = new ArrayList[5];

  • 聲明一個變量 list,它指向 ArrayList 數組 類型的對象
  • 在堆裏創建了一個能裝 5 個 ArrayList 對象的數組。
  • 編譯器和 JVM 都知道:這個數組的類型是“ArrayList 的數組”。
  • 它可以裝任何種類的 ArrayList(比如裝 ArrayList<Integer>ArrayList<String> 等)。

②ArrayList<String>[] listArr = list;

  • 編譯器角度:

我要聲明一個變量 listArr,它的類型是 ArrayList<String>[],也就是説,這個變量在語義上只能指向裝 ArrayList<String> 的數組。
但右邊其實給我的 list 是個 ArrayList[](原始類型數組),
規範允許:如果你使用了 raw type(比如 ArrayList),編譯器允許把它賦給一個帶泛型參數的類型,雖然不安全,我就讓它過吧,但我得給個 unchecked warning。”

也就是説

list 的靜態類型是 ArrayList[],所以它認為:

list 是一個能裝 ArrayList 對象的數組。”

listArr 的靜態類型是 ArrayList<String>[],所以它認為:

listArr 是一個能裝 ArrayList<String> 對象的數組。”

  • 運行時角度(JVM):
    “行,我就讓兩個變量 listArrlist 指向同一塊數組內存。”

Java泛型詳解,史上最全圖文詳解_泛型_12

於是,list和listArr都指向同一個地址

只是編譯器以為” listArr 裏面的每個元素都是 ArrayList<String>

而運行起來的 JVM 知道:“其實就是普通的 ArrayList 啊。”

③ArrayList<Integer> intList = new ArrayList<>();
intList.add(100);
list[0] = intList;

講一下流程:

在棧內創建一個變量(引用變量),名字叫 intList;在堆內創建一個新的 ArrayList 對象,類型是 ArrayList<Integer>,然後讓 intList 指向這個對象。

然後往 intList 所指向的 ArrayList 對象中添加一個元素 100。所以現在堆內的那個 ArrayList 對象狀態是 [100]

數組 list 的元素類型是 ArrayList,每個格子(list[0]list[1] …)存放的不是對象本身,而是對象的引用(地址)。

④String s = listArr[0].get(0);出問題了!

  • 編譯時
    編譯器看到 listArrArrayList<String>[]
    它想:“你取出來的肯定是個 String,我就讓你賦給 String s 吧。”
  • 運行時
    JVM 去 listArr[0] 看,發現那是 ArrayList<Integer>
    它取出了 Integer 100,嘗試給 String s
    就拋出了異常!ClassCastException