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; }
}
當別人使用時, Integer、String 代替了 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 及其子類的對象(比如 Integer、Double)。
所以:我寫如下代碼沒問題
Box<Number> box1 = new Box<>();
box1.setFirst(100);
showBox(box1);
但是寫如下代碼會出問題
Box<Integer> box2 = new Box<>();
box2.setFirst(200);
showBox(box2);//這裏會報錯
為什麼會報錯?
Integer 是 Number 的子類
但 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> 里加 Cat 或 MiniCat,當然安全。
但如果反過來你想 Cat c = list.get(0),編譯器拒絕:
——因為它不知道你傳進來的是不是 List<Object>,如果是Object,你為什麼要用Cat接收? 安全起見,只能當作 Object 讀取。
5.4 類型通配符的下限(2)
這裏講了應用 以TreeSet為例,其中有兩個構造方法
我們講第一個,傳構造器的
我們創建了三個類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排序
如果傳入父類Animal的比較器呢?
發現也是可以的
如果傳入子類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());
}
運行後發現
他們的類都是一樣的,所以這也就解釋了,為什麼我們編寫代碼的時候,看似類型不一樣,一個是ArrayList<Integer>、另外一個是 ArrayList<String>,但是運行後其實是歸屬於一個類型,因為Java 的泛型在編譯後被擦除
6.1 無限制類型擦除
什麼意思呢?當我的泛型標識為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());
}
}
}
輸出:
所以,如果泛型類的泛型列表只有一個T,那類型擦除之後,確實是類型轉換成了Object
6.2 有限制類型擦除
什麼意思呢?當我的泛型標識為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());
}
}
}
輸出為:
所以,如果泛型類的泛型列表是T extends Number,那類型擦除之後,確實是類型轉換成了Number
上面我説是泛型類,泛型方法也一樣!!
6.3 橋接方法
橋接方法存在於泛型接口中
可以看見,泛型標識被擦除成了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());
}
運行結果如下
第二個info就是生成的橋接方法
7、泛型與數組
7.1 數組的協變
講正式內容之前,先講一下數組的協變
想象你有個動物園系統:
Cat是Animal的子類。- 一個
Animal[]數組可以裝各種動物。
那你説:能不能用一個 Cat[] 去當作 Animal[] 使用?
Java 説:可以!
Cat[] cats = new Cat[3];//這是新創建了一個數組,數組引用是放在cats變量裏,指向的是實際的地址Cat[3]
Animal[] animals = cats; // ✅ 允許 將剛才的地址給animals,現在,animals和cats都存放着同樣的引用,指向實際的地址Cat[3]
這就是 協變 —— Cat 是 Animal 的子類型,所以 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.Array的newInstance(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()));
輸出:
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):
“行,我就讓兩個變量listArr和list指向同一塊數組內存。”
於是,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);出問題了!
- 編譯時:
編譯器看到listArr是ArrayList<String>[]。
它想:“你取出來的肯定是個String,我就讓你賦給String s吧。” - 運行時:
JVM 去listArr[0]看,發現那是ArrayList<Integer>。
它取出了Integer 100,嘗試給String s。
就拋出了異常!ClassCastException。