泛型是 Java 開發中常用的技術,了解泛型的幾種形式和實現泛型的基本原理,有助於寫出更優質的代碼。本文總結了 Java 泛型的三種形式以及泛型實現原理。
1. 泛型
泛型的本質是對類型進行參數化,在代碼邏輯不關注具體的數據類型時使用。例如:實現一個通用的排序算法,此時關注的是算法本身,而非排序的對象的類型。
1.1 泛型方法
如下定義了一個泛型方法,
class GenericMethod{
public <T> T[] sort(T[] elements){
return elements;
}
}
1.2 泛型類
與泛型方法類似,泛型類也需要聲明類型變量,只不過位置放在了類名后面,作用的范圍包括了當前中的成員變量類型,方法參數類型,方法返回類型,以及方法內的代碼中。
子類繼承泛型類時或者實例化泛型類的對象時,需要指定具體的參數類型或者聲明一個參數變量。如下,SubGenericClass 繼承了泛型類 GenericClass,其中類型變量 ID 的值為 Integer,同時子類聲明了另一個類型變量 E,並將E 填入了父類聲明的 T 中。
class GenericClass<ID, T>{
}
class SubGenericClass<T> extends GenericClass<Integer, T>{
}
1.3 泛型接口
泛型接口與泛型類類似,也需要在接口名后面聲明類型變量,作用於接口中的抽象方法返回類型和參數類型。子類在實現泛型接口時需要填入具體的數據類型或者填入子類聲明的類型變量。
interface GenericInterface<T> {
T append(T seg);
}
2. 泛型的基本原理
泛型本質是將數據類型參數化,它通過擦除的方式來實現。聲明了泛型的 .java 源代碼,在編譯生成 .class 文件之后,泛型相關的信息就消失了。可以認為,源代碼中泛型相關的信息,就是提供給編譯器用的。泛型信息對 Java 編譯器可以見,對 Java 虛擬機不可見。
Java 編譯器通過如下方式實現擦除:
- 用 Object 或者界定類型替代泛型,產生的字節碼中只包含了原始的類,接口和方法;
- 在恰當的位置插入強制轉換代碼來確保類型安全;
- 在繼承了泛型類或接口的類中插入橋接方法來保留多態性。
Java 官方文檔原文
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
下面通過具體代碼來說明 Java 中的類型擦除。
實驗原理:先用 javac 將 .java 文件編譯成 .class 文件,再使用反編譯工具 jad 將 .class 文件反編成回 Java 代碼,反編譯出來的 Java 代碼內容反映的即為 .class 文件中的信息。
如下源代碼,定義 User 類,實現了 Comparable 接口,類型參數填入 User,實現 compareTo 方法。
class User implements Comparable<User> {
String name;
public int compareTo(User other){
return this.name.compareTo(other.name);
}
}
JDK 中 Comparable 接口源碼內容如下:
package java.lang;
public interface Comparable<T>{
int compareTo(T o);
}
我們首先反編譯它的接口,Comparable 接口的字節碼文件,可以在 $JRE_HOME/lib/rt.jar
中找到,將它復制到某個目錄。使用 jad.exe(需要另外安裝)反編譯這個 Comparable.class 文件。
$ jad Comparable.class
反編譯出來的內容放在 Comparable.jad 文件中,文件內容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Comparable.java
package java.lang;
// Referenced classes of package java.lang:
// Object
public interface Comparable
{
public abstract int compareTo(Object obj);
}
對比源代碼 Comparable.java 和反編譯代碼 Comparable.jad 的內容不難發現,反編譯之后的內容中已經沒有了類型變量 T 。compareTo 方法中的參數類型 T 也被替換成了 Object。這就符合上面提到的第 1 條擦除原則。這里演示的是用 Object 替換類型參數,使用界定類型替換類型參數的例子可以反編譯一下 Collections.class 試試,里面使用了大量的泛型。
使用 javac.exe 將 User.java 編譯成 .class 文件,然后使用 jad 將 .class 文件反編譯成 Java 代碼。
$ javac User.java
$ jad User.class
User.jad 文件內容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: User.java
class User
implements Comparable
{
User()
{
}
public int compareTo(User user)
{
return name.compareTo(user.name);
}
// 橋接方法
public volatile int compareTo(Object obj)
{
return compareTo((User)obj);
}
String name;
}
對比編輯的源代碼 User.java 和反編譯出來的代碼 User.jad,容易發現:類型參數沒有了,多了一個無參構造方法,多了一個 compareTo(Object obj) 方法,這個就是橋接方法,還可以發現參數 obj 被強轉成 User 再傳入 compareTo(User user) 方法。通過這些內容可以看到擦除規則 2 和規則 3 的實現方式。
強轉規則比較好理解,因為泛型被替換成了 Object,要調用具體類型的方法或者成員變量,當然需要先強轉成具體類型才能使用。那么插入的橋接方法該如何理解呢?
如果我們只按照下面方式去使用 User 類,這樣確實不需要參數類型為 Object 的橋接方法。
User user = new User();
User other = new User();
user.comparetTo(other);
但是,Java 中的多態特性允許我們使用一個父類或者接口的引用指向一個子類對象。
Comparable<User> user = new User();
而按照 Object 替換泛型參數原則,Comparable 接口中只有 compareTo(Object) 方法,假設沒有橋接方法,顯然如下代碼是不能運行的。所以 Java 編譯器需要為子類(泛型類的子類或泛型接口的實現類)中使用了泛型的方法額外生成一個橋接方法,通過這個方法來保證 Java 中的多態特性。
Comparable<User> user = new User();
Object other = new User();
user.compareTo(other);
而普通類中的泛型方法在進行類型擦除時不會產生橋接方法。例如:
class Dog{
<T> void eat(T[] food){
}
}
類型擦除之后變成了:
class Dog
{
Dog()
{
}
void eat(Object aobj[])
{
}
}
3. 小結
Java 中的泛型有 3 種形式,泛型方法,泛型類,泛型接口。Java 通過在編譯時類型擦除的方式來實現泛型。擦除時使用 Object 或者界定類型替代泛型,同時在要調用具體類型方法或者成員變量的時候插入強轉代碼,為了保證多態特性,Java 編譯器還會為泛型類的子類生成橋接方法。類型信息在編譯階段被擦除之后,程序在運行期間無法獲取類型參數所對應的具體類型。
參考
https://docs.oracle.com/javase/tutorial/java/generics/index.html
https://stackoverflow.com/questions/25040837/generics-bridge-method-on-polymorphism