帶着問題閱讀
1、什么是Java泛型,有什么用處
2、Java泛型的實現機制是什么
3、Java泛型有哪些局限和限制
Java泛型介紹
引入泛型之前,試想編寫一個加法器,為處理不同數字類型,就需要對不同類型參數進行重載,但其實現內容是完全一樣的,如果是一個更復雜的方法,無疑會造成重復。
public int add(int a, int b) {return a + b;}
public float add(float a, float b) {return a + b;}
public double add(double a, double b) {return a + b;}
一般的類和方法,只能使用具體的類型,要么是基本類型,要么是自定義的類。如果要編寫可以應用於多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。《Java編程思想》
Java在1.5版本引入泛型,通過泛型實現的加法代碼可簡化為:
public <T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
泛型的核心概念是參數化類型,使用參數指定方法類型,而非硬編碼。泛型的出現帶給我們很多好處,其中最重要的莫過於對集合類的改進,避免了任意類型都可以丟到同一個集合里的不可靠問題。
然而Python和Go的集合可以容納任意類型,這究竟是進步還是退步呢
Java泛型使用簡介
泛型一般有三種使用方式:泛型類、泛型接口和泛型方法。
泛型類
public class GenericClass<T> {
private T member;
}
...
// 初始化時指定泛型類型
GenericClass<String> instance = new GenericClass<String>();
泛型接口
public interface GenericInterface<T> {
void test(T param);
}
// 實現類指定泛型類型
public class GenericClass implements GenericInterface<String> {
@Override
public void test(String param) {...}
}
泛型方法
如前文中加法代碼的實現就是泛型方法。
// 在方法前添加<T>,泛型類型可用於返回值也可用於參數
public <T> T function(T param);
...
function("123"); // 編譯器自動識別T為String
深入Java泛型
Java的偽泛型和類型擦除
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); //true
對如上部分代碼,相信多數人接觸到泛型的第一時刻都認為這是兩個不同的類型,反編譯其字節碼獲得代碼如下:
ArrayList var1 = new ArrayList();
ArrayList var2 = new ArrayList();
System.out.println(var1.getClass() == var2.getClass());
我們發現兩個列表都變成ArrayList類型,如果大家對Jdk1.5之前的版本還有印象就可以看出,這一段反編譯的代碼就是Java集合最初的使用形式。因此,Java泛型的是通過編譯期將泛型的實際類型擦除為原始類型(通常為Object)實現的偽泛型。
所謂偽泛型,是相對C++的"真泛型"(異構擴展,可見參考第三條),在Java中,由於編譯后擦除了具體類型,在泛型代碼內部,無法獲得任何有關泛型參數類型的信息,在運行期代碼所持有的也只是擦除后的原始類型,也就意味着在運行期可以通過反射的方式為泛型類傳入任何原始類型的參數。
public class GenericTest {
public List<Integer> ints = new ArrayList<>();
public static void main(String[] args) {
GenericTest test = new GenericTest();
List<GenericTest> list = (List<GenericTest>) GenericTest.class.getDeclaredField("ints").get(test);
list.add(new GenericTest());
System.out.println(test.ints.get(0)); // 打印GenericTest變量地址
int number = test.ints.get(0); // 類型轉換拋出異常
}
}
// 泛型代碼內部是指泛型類或泛型方法內部。
public class Generic<T> {
public Class getTClass() { //無法獲取 }
}
public <T> Class getParamClass(T param) { //無法獲取 }
在泛型外部可以獲取已指定的泛型參數類型,通過javap -v
查看Constant Pool
,可看到具體類型記錄在Signature
。
public class Outer {
private List<String> list = new ArrayList<>(); //可以獲取list的具體類型
}
事實上在Java推出泛型時,C++的模板泛型已經相當成熟,設計者也並非沒有能力實現包含具體類型的泛型,使用類型擦除最重要的原因還是為了保持兼容性。假設ArrayList<String>
和ArrayList
編譯后是不同的class,那么為了兼容舊代碼正常運行,必須平行的添加一套泛型集合並在后續版本中同時維護,而集合類作為大量使用的基礎工具類,開發者不得不為此承擔大量代碼切換的風險(參考Vector
和HashTable
的帶來的遺留問題),因此相較於兼容性的取舍,采用類型擦除實現泛型算是折中方案。
思考一下,下面的類可以編譯通過嗎
public class Test { void test(List<String> param) {} void test(List<Integer> param) {} }
Java泛型的上下界
前面說到泛型會被擦除為原始類型,一般是Object
。如果泛型聲明為<? extends Number>
,就會被擦除為Number
。
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
numbers = integers; // compile error
考慮以上代碼,numbers
可以增加Integer
類型的元素,直覺上integers
應該也可以賦值給numbers
。由於類型擦除,Java在編譯期限定了只有相同類型的泛型實例才可以互相賦值,但這樣就違背了Java的多態,為了解決泛型轉換的問題,Java引入了上下限<? extends A>
和<? super B>
兩種機制。
如果泛型聲明為<? extends A>
,即聲明該泛型的上界也即擦除后的原始類型為A
,同時該泛型類的實例可以引用A
子類的泛型實例。
// 上界保證取出來的元素一定是Number,但無法約束放入的類型
List<Integer> integers = new ArrayList<>();
List<Float> floats = new ArrayList<>();
List<? extends Number> numbers = integers; // numbers = floats; 也可以
numbers.get(0); // ok,總能保證取出的一定是Number
numbers.put(1); // compile error,無法保證放入的是否符合約束
如果泛型聲明為<? super B>
,即聲明該泛型的下界為B
,原始類型仍為Object
,同時該泛型類的實力可以引用B
父類的泛型實例。
// 假設三個繼承類 Child -> Father -> GrandFather
// 下界保證寫入的元素一定是Child,但無法確定取出的類型
List<Father> fathers = new ArrayList<>();
List<GrandFather> grandFathers = new ArrayList<>();
List<? super Child> childs = fathers; // childs = grandFathers; 也可以
numbers.put(new Child()); //ok,總能保證實際容器可接受Child
Child ele = (Child) numbers.get(0); // runtime error,無法確定得到的具體類型
在Java中,根據里式替換原則,向上轉型是默認合法的,向下轉型則需要強制轉換,如不能轉換則報錯。在extends
的get
和super
的put
場景中,一定可以保證讀取/放入的元素是可以向上轉型的,而在extends
的put
和super
的get
中,則無法確認可轉的類型,因此extends
只能讀取,super
只能寫入。
當然如果使用super時,取出的對象以Object存放,也沒有問題,因為super擦除后的原始類型為Object。
參考《Effective Java》中給出的PECS
使用建議。
為了獲得最大限度的靈活性,要在表示生產者或消費者的輸入參數上使用通配符類型。
如果參數化類型表示一個T生產者,就使用<? extends T>。 producer-extends
如果參數化類型表示一個T消費者,就使用<? super T>。consumer-super
如果某個輸入參數即是生產者又是消費者,那么通配符類型對你就沒什么好處了。
這一段話筆者認為有一定迷惑性,生產者是寫入的,消費者是讀取的,前文介紹過extends
用於讀取,而super
用於寫入,恰恰相反。
個人認為對這段話的正確理解是以泛型為第一視角切入,即當泛型類型本身作為生產者提供功能(被讀取)時使用extends
,反之(被寫入)使用super
。而非常規意義上生產者要寫入的容器采用extends
,消費者讀取的容器使用super
。
// producer,此時返回值作為生產后的結果提供給消費者
List<? extends A> writeBuffer(...);
// consumer,此時返回值作為消費后的結果提供給生產者
List<? super B> readBuffer(...);
Java泛型的多態
泛型類也可以被繼承,泛型類主要有兩種繼承方式。
public class Father<T> { public void test(T param){} }
// 泛型繼承,Child依然是泛型類
public class Child<T> extends Father<T> {
@Override
public void test(T param){}
}
// 指定泛型類型,StringChild為具體類
public class StringChild extends Father<String> {
@Override
public void test(String param){}
}
我們知道@Override
是保持簽名不變且重寫父類方法,查看Father
類字節碼,其中test方法被擦除為void test(Object param)
;在StringChild
中,方法簽名為void test(String param)
。到此讀者可能意識到,這根本不是重寫而是重載(Overload
)。
查看StringChild
的字節碼。
...
#3 = Methodref
...
public void test(java.lang.String);
...
invokespecial #3 // Method StringChild.test:(Ljava/lang.Object;) V
...
public void test(java.lang.Object);
可以看到其中實際包含了兩個方法,一個參數是String
一個是Object
,后者才是對父類方法的重寫,前者通過invoke轉到對后者的調用。這個方法是JVM在編譯時自動添加的,也叫做橋方法。同時還有一點需要提及,示例中的代碼是以泛型參數作為入參,作為返回類型的話會產生Object test()
和String test()
兩個方法,這兩個方法在常規的編碼中是無法編譯通過的,但JVM為泛型多態的實現允許了這個不合規的存在。
Java泛型使用的局限
- 基本類型無法擦除為原始類型
Object
,因此范型不支持基本類型
List<int> intList = new ArrayList<>(); // 無法編譯
- 由於類型擦除,泛型代碼內部在運行期無法獲得具體類型
T instance = new T(); // 不能直接使用泛型初始化
if (t instanceOf T); // 不能判斷泛型類型
T[] array = new T[]{}; // 不能創建泛型數組
- 由於靜態資源加載先於類型實例化,不能在靜態代碼塊引用泛型
// Error
public class Generic<T> {
public static T t;
public static T test() {return t;}
}
- 異常類型的派生類不能添加泛型
// 假設繼承實現一個泛型異常
class SomeException<T> extends Exception...
try {
...
} catch(SomeException<Integer> | SomeException<String> ex) {
//由於類型擦除,無法捕獲多個泛型異常
...
}
參考
- 《Java編程思想》
- 《Effective Java》
- Java不能實現真正泛型的原因
- Java泛型機制詳解