Java的泛型詳解
泛型的好處
- 編寫的代碼可以被不同類型的對象所重用。
- 因為上面的一個優點,泛型也可以減少代碼的編寫。
泛型的使用
簡單泛型類
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst(){
return first;
}
public T getSecond(){
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
- 上面例子可以看出泛型變量為T;
- 用尖括號(<>)括起來,並放在類名后面;
- 泛型還可以定義多個類型變量比如上面的例子 first和second不同的類型:
public class Pair<T, U> {....}
注: 類型變量的定義需要一定的規范:
(1) 類型變量使用大寫形式,並且要比較短;
(2)常見的類型變量特別代表一些意義:變量E 表示集合類型,K和V表示關鍵字和值的類型;T、U、S表示任意類型;
- 類定義的類型變量可以作為方法的返回類型或者局部變量的類型;
例如: private T first;
- 用具體的類型替換類型變量就可以實例化泛型類型;
例如: Pair<String> 代表將上述所有的T 都替換成了String
- 由此可見泛型類是可以看作普通類的工廠
泛型方法
- 我們應該如何定義一個泛型方法呢?
- 泛型的方法可以定義在泛型類,也可以定義在普通類,那如果定義在普通類需要有一個尖括號加類型來指定這個泛型方法具體的類型;
public class TestUtils {
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}
- 類型變量放在修飾符(static)和返回類型的中間;
- 當你調用上面的方法的時候只需要在方法名前面的尖括號放入具體的類型即可;
String middle = TestUtils.<String>getMiddle("a", "b", "c");
如果上圖這種情況其實可以省略
String middle = TestUtils.getMiddle("a", "b", "c");
但是如果是以下調用可能會有問題:
如圖:可以看到變意思沒有辦法確定這里的類型,因為此時我們入參傳遞了一個Double3.14
兩個Integer1729
和0
編譯器認為這三個不屬於同一個類型;
此時有一種解決辦法就是把整型寫成Double類型
類型變量的限定
- 有時候我們不能無限制的讓使用者傳遞任意的類型,我們需要對我們泛型的方法進行限定傳遞變量,比如如下例子
計算數組中最下的元素
- 這個時候是無法編譯通過的,且編譯器會報錯
- 因為我們的編譯器不能確定你這個T 類型是否有
compareTo
這個函數,所以這么能讓編譯器相信我們這個T是一定會有compareTo
呢? - 我們可以這么寫
<T extends Comparable>
這里的意思是T一定是繼承Comparable
的類 - 因為
Comparable
是一定有compareTo
這個方法,所以T一定有compareTo
方法,於是編譯器就不會報錯了 - 因為加了限定那么
min
這個方法也只有繼承了Comparable
的類才可以調用; - 如果要限定方法的泛型繼承多個類可以加
extends
關鍵字並用&
分割如:T extends Comparable & Serializable
- 限定類型是用
&
分割的,逗號來分割多個類型變量<T extends Comparable & Serializable , U extends Comparable>
類型擦除
不論什么時候定義一個泛型類型,虛擬機都會提供一個相應的原始類型(raw type)。原始類型的名字就是刪掉類型參數后的泛型類型。擦除類型變量,並替換限定類型(沒有限定類型的變量用Object)
列如: Pair
的原始類型如下所示
public class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
second = null;
}
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst(){
return first;
}
public Object getSecond(){
return second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
}
- 因為上面的T是沒有限定變量,於是用Object代替了;
- 如果有限定變量則會以第一個限定變量替換為原始類型如:
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
}
- 原始類型如下所示:
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
}
翻譯泛型表達式
- 上面說到泛型擦除類型變量后對於無限定變量后會以Object來替換泛型類型變量;
- 但是我們使用的時候並不需要進行強制類型轉換;
- 原因是編譯器已經強制插入類型轉換;
例如:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
- 擦除getFirst的返回類型后將返回Object類型,但是編譯器自動插入Employee的強制類型轉換,編譯器會把這個方法調用翻譯為兩條虛擬機指令;
- 對原始方法Pair.getFirst的調用
- 將返回的Object類型強制轉換為Employee類型;
我們可以反編譯驗證一下
關鍵的字節碼有以下兩條
9: invokevirtual #4 // Method com/canglang/Pair.getFirst:()Ljava/lang/Object;
12: checkcast #5 // class com/canglang/model/Employee
虛擬機指令含義如下:
- invokevirtual:虛函數調用,調用對象的實例方法,根據對象的實際類型進行派發,支持多態;
- checkcast:用於檢查類型強制轉換是否可以進行。如果可以進行,checkcast指令不會改變操作數棧,否則它會拋出ClassCastException異常;
由此我們可以驗證了上述的結論,在反編譯后的字節碼中看到,當對泛型表達式調用時,虛擬機操作如下:
- 對於對象的實際類型進行替換泛型;
- 檢查類型是否可以強制轉換,如果可以將對返回的類型進行強制轉換;
翻譯泛型方法
類型擦除也會出現在泛型方法里面
public static <T extends Comparable> T min(T[] a)
類型擦除后
public static Comparable min(Comparable[] a)
此時可以看到類型參數T已經被擦除了,只剩下限定類型Comparable;
方法的類型擦除帶來了兩個復雜的問題,看下面的示例:
public class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second){
System.out.println("DateInterval: 進來這里了!");
}
}
此時有個問題,從Pair繼承的setSecond方法類型擦除后為
public void setSecond(Object second)
這個和DateInterval的setSecond明顯是兩個不同的方法,因為他們有不同的類型的參數,一個是Object,一個LocalDate;
那么看下面一個列子
public class Test {
public static void main(String[] args) {
DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(LocalDate.of(2020, 5, 20));
}
}
Pair引用了DateInterval對象,所以應該調用DateInterval.setSecond。
我們看一下運行結果
但是看了反編譯的字節碼可能發現一個問題:
17: invokestatic #4 // Method java/time/LocalDate.of:(III)Ljava/time/LocalDate;
20: invokevirtual #5 // Method com/canglang/Pair.setSecond:(Ljava/lang/Object;)V
這里可以看到此處字節碼調用的是Pair.setSecond
這里有個重要的概念就是橋方法
Oracle中對於這個現象的解釋
為了解決此問題並在類型擦除后保留通用類型的 多態性,
Java編譯器生成了一個橋接方法,以確保子類型能夠按預期工作。
對於DateInterval類,編譯器為setSecond生成以下橋接方法:
public class DateInterval extends Pair {
// Bridge method generated by the compiler
//
public void setSecond(Object second) {
setSecond((LocalDate)second);
}
public void setSecond(LocalDate second){
System.out.println("DateInterval: 進來這里了!");
}
}
那么我們如何驗證是否生成這個橋方法呢?我們可以反編譯一下DateInterval.java看一下字節碼;
public void setSecond(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #5 // class java/time/LocalDate
5: invokevirtual #6 // Method setSecond:(Ljava/time/LocalDate;)V
8: return
我截取了部分發現在 DateInterval的字節碼中的確會有一個橋方法,同時驗證了上面的問題;
總結
- 虛擬機中沒有泛型,只有普通的類和方法
- 所有的類型參數都用他們的限定類型替換
- 橋方法被合成來保持多態
- 為保持類型安全性,必要時插入強制類型轉換