泛型基礎 (三) — Java 與 Kotlin 向下相容、Type Erasure 和 Reifiable

Jimmy Liu
Kuo’s Funhouse
Published in
13 min readDec 17, 2021

Java 5 推出泛型後要如何做到向下相容呢?

大家會看這篇應該已經了解何謂泛型和變型,以及 Java 與 Kotlin 之間實作泛型的不同吧~

若還不懂,那就可以看看這兩篇喔 ~

在第一篇有提到,在還沒有泛型前,Java 只能用 raw type 來建立物件:

List list = new ArrayList(); // raw type

Raw Type 因威無法讓開發者清楚地知道 List 裡面放的是什麼型別,所以很容易有 runtime error。

當泛型導入後,我們不僅可以在編譯時就能即時偵測錯誤還讓開發團隊對類別所支持的型別一目了然:

List<Integer> list = new ArrayList<>();

但是,你可想過 Java 5 是如何做到向下相容呢? 其實答案很簡單,那就是:

Type Erasure

型別擦除 (Type Erasure)

如同其名,Type Erasure 會在編譯時將型別給擦除

那麼 Type Erasure 都做了什麼呢?

  1. 替換泛型與變型:用變型的上下限型別來替換原本的泛型和變型,如此一來只會留下最簡單的型別
  2. 類型轉換:再取代變型的同時,為了確保型別安全性,需要的時候會插入類型轉換
  3. 橋接方法:建立橋接的方法以確保多型特性

說了這麼多還不如直接手動。要看 Type erasure 的功效,最容易的就是看 Kotlin Decompile 的結果。

我們用回去之前的 KBigBox 類別吧:

Decompile KBigBox <T, out O, in I>

替換泛型與變型

class KBigBox<T, out O, in I> {
private var _t: T? = null
private var _o: O? = null
private var _i: I? = null
private val ls: List<T> = listOf()
private val ml: MutableList<T> = mutableListOf()

fun getT(): T? = _t
fun getO(): O? = _o
fun getI(): @UnsafeVariance I? = _i

fun setT(t: T) { _t = t }
fun setO(o: @UnsafeVariance O) { _o = o }
fun setI(i: I) { _i = i }
}

當我按下 Decompile 時,我會看到以下:

public final class KBigBox {
private Object _t;
private Object _o;
private Object _i;
private final List ls = CollectionsKt.emptyList();
private final List ml = (List)(new ArrayList());
@Nullable
public final Object getT() { return this._t; }

@Nullable
public final Object getO() { return this._o; }

@Nullable
public final Object getI() { return this._i; }

public final void setT(Object t) { this._t = t; }

public final void setO(Object o) { this._o = o; }

public final void setI(Object i) { this._i = i; }
}

可見,編譯器將所有的泛型與變型用 Object 來取代。

那麼,如果給出繼承的上下限呢?

Decompile JBigBox <T: Number, out O: Number, in I: Number>

類型轉換

class JBigBox <T: Number, out O: Number, in I:Number>{
private var _t: T? = null
private var _o: O? = null
private var _i: I? = null
private val ls: List<T> = listOf()
private val ml: MutableList<T> = mutableListOf()
fun getT(): T? = _t
fun getO(): O? = _o
fun getI(): @UnsafeVariance I? = _i

fun setT(t: T) { _t = t }
fun setO(o: @UnsafeVariance O) { _o = o }
fun setI(i: I) { _i = i }
}

在 Decompile 後:

public final class JBigBox {
private Number _t;
private Number _o;
private Number _i;
private final List ls = CollectionsKt.emptyList();
private final List ml = (List)(new ArrayList());
@Nullable
public final Number getT() { return this._t; }

@Nullable
public final Number getO() { return this._o; }

@Nullable
public final Number getI() { return this._i; }

public final void setT(@NotNull Number t) {
Intrinsics.checkNotNullParameter(t, "t");
this._t = t;
}

public final void setO(@NotNull Number o) {
Intrinsics.checkNotNullParameter(o, "o");
this._o = o;
}

public final void setI(@NotNull Number i) {
Intrinsics.checkNotNullParameter(i, "i");
this._i = i;
}
}

那麼,如果是繼承的效果呢?

Decompile LBigBox: KBigBox<Number, Number, Number>()

橋接方法

class LBigBox: KBigBox<Number, Number, Number>() {
override fun setT(t: Number) = super.setT(t)
override fun setI(i: Number) = super.setI(i)
override fun setO(o: Number) = super.setO(o)
}

結果是:

public final class LBigBox extends KBigBox {
public void setT(@NotNull Number t) {
Intrinsics.checkNotNullParameter(t, "t");
super.setT(t);
}

// $FF: synthetic method
// $FF: bridge method
public void setT(Object var1) {
this.setT((Number)var1);
}

public void setI(@NotNull Number i) {
Intrinsics.checkNotNullParameter(i, "i");
super.setI(i);
}

// $FF: synthetic method
// $FF: bridge method
public void setI(Object var1) {
this.setI((Number)var1);
}

public void setO(@NotNull Number o) {
Intrinsics.checkNotNullParameter(o, "o");
super.setO(o);
}

// $FF: synthetic method
// $FF: bridge method
public void setO(Object var1) {
this.setO((Number)var1);
}
}

從 Decompile 結果我們可以看到 Type Erasure 運行效果如下:

替換泛型與變型

所有的泛型都會被上限或下限類型所替換,預設為 Object

private var _t: T? = null => private Object _t; // KBigBox
private var _t: T? = null => private Number _t; // JBigBox

類型轉換

為了型別安全性,編輯器會插入適當的型別

private val ml: MutableList<T> = mutableListOf()
// 轉換成
private final List ml = (List)(new ArrayList());

橋接方法

雖然 LBigBox 將 T 設為 Number,並 override set 方法,但是 LBigBox 是繼承了 KBigBox 的。 所以編譯器會先將 KBigBoxset 方法設為 Object,然後再建立一個 橋接方法 來將 KBigBoxset 接上 LBigBoxset

   public void setI(@NotNull Number i) {
Intrinsics.checkNotNullParameter(i, "i");
super.setI(i);
}

// $FF: synthetic method
// $FF: bridge method
public void setI(Object var1) {
this.setI((Number)var1);
}

現在應該暸解 Type Erasure 的功用了。

雖說 Type Erasure 可以擦掉所有的泛型型別,但是也有一些類別是不會被刪除了,像是 Java 中的 Array。

val ar = emptyArray<Int>()

當類型擦除後,Array 的型別還是會被保存:

Integer[] ar = new Integer[0];

這些類型就被稱為 Reifiable Type

Reifiable Type

可實體化的類型指的是可以在 runtime 讀取它的類型資料,這類型包括了:Primitives、Non-generic type、Raw type 還有 <?> (Unbound Wildtype)

那是不是我們可以隨時取得可實體化類型的泛型資料?

並不是的,因為對編譯器而言,這個方法看起來是這樣:

public static final void getType(@NotNull Object[] array) {
Intrinsics.checkNotNullParameter(array, "array");
}

編譯器會將方法中的泛型型別擦去,導致最後只剩下 Object

那該怎麼辦呢?

reified

我們這時需要跟編譯器說明這個泛型型別是可被實體化的:

fun <reified U> getType(array: Array<U>)

但這樣還是不夠:

因為這方法並不是只用在一個地方,也就是說不一定會給相同型別使用。

getType(emptyArray<String>())getType(emptyArray<Int>())//...getType(emptyArray<T>())

所以這方法必須在拷貝在每個使用的地方。

這時我們就需要另外一個關鍵字:

inline

inline fun <reified U> getType(array: Array<U>) {}

Decompile getType

我先定義 getType 的內容 (如果沒有讀取 泛型 資料,編譯器就不會理它)

inline fun <reified U> getType(array: Array<U>) {
println(U::class.java)
}

Decompile 後的結果如下 :

public static final void getType(Object[] array) {
int $i$f$getType = 0;
Intrinsics.checkNotNullParameter(array, "array");
Intrinsics.reifiedOperationMarker(4, "U");
Class var2 = Object.class;
System.out.println(var2);
}

但要看到 inline 的效果,那就要呼叫它:

getType(emptyArray<Int>())
getType(emptyArray<String>())

Decompile 結果就會如下:

// getType(emptyArray<Int>())
Object[] array$iv = new Integer[0];
int $i$f$getType = false;
Class var6 = Integer.class;
System.out.println(var6);
// getType(emptyArray<String>())
Object[] array$iv = new String[0];
$i$f$getType = false;
var6 = String.class;
System.out.println(var6);

補充

我想在最後補充一下關於 inline 的用法。

首先,inline 的用法不止於此。

由於在 Kotlin 中,高級方法會被編譯成新的物件,因此為了減少 overhead ,有的時候會使用 inline 來告知編譯器這方法不需要製作成物件。

當然,除非你的高級方法會再度被傳入其他方法內,那麼這就必須要是物件才能帶入了。

另外,官方建議如果你想用 inline,你的方法最好不要太長,否則你的 code 編譯後會變得很長。

結論

Java5 為了要向下相容,所以有了 Type Erasure 的機制在編譯時將非實體化的型別都擦去。

實體化型別包括了: Primitives、Non-generic type、Raw type 還有 <?>

要避免被擦去,就得使用 inlinereified 關鍵字:

inline fun <reified U> getType(array: Array<U>)

reified 只能用在 extension 或是 static 的方法,因為我們無法定義出一個 inline 的方法。

有不明白的或是有寫錯的,歡迎留言指教。

如果這篇文章對您有幫助,那就請您分享和給我點掌聲吧 ~~

--

--

Jimmy Liu
Kuo’s Funhouse

App Developer who enjoy learning from the ground up.