泛型基礎 (一) — Java 與 Kotlin 的變型
參數多型 (Parametric Polymorphism)是一種將類型參數化進而讓程式設計更為彈性的編程概念。 這便是大家所熟知的 Generic (泛型)。
泛型
泛型是 1975年 在 ML (Meta Language) 中推出的程式編寫風格。
泛性的主要特色就是型別 (類型與介面) 的參數化 (Parameterized)。
經由參數化的型別,我們可以在建立具有泛型型別的物件時,將目標型別代入其中。
以下是 使用泛型前 與 使用泛型後 的比較:
假設我們現在有個能像盒子一樣存取任何物件的類別 (Box)
因為無法使用泛型,因此放入 Box 中的物件會以 Object 做為參數類別。
由於無法知道內部的 object 是那個類別,因此很有可能會發生放入和期許取出的類別不一致。
但是因為 compiler 無法偵測,所以會直接導致 runtime error。
Box box = new Box();
box.set(1);
String str = (String)box.get(); // runtime error
若使用了泛型,我們就能將 Box 定義成一個 泛型類型 (Generic Type):
其中,我們稱 <T> 為類型參數 (Type Parameter)。
當我們要使用或建立這泛型類型時,我們就可以給予 <T> 一個特定的類型並形成 參數類別 (Parameterized Type, Box<String>):
Box<Integer> intBox = new Box<>();
Box<String> strBox = new Box<>();// raw type
// 會將 T 設成 Object
Box rawBox = new Box();
這下就建立了 參數類別 (Parameterized Type)。
在 Java 中,使用泛型後其中之一的重要特色就是 編輯器 可以即時偵測類別是否正確並即時匯報錯誤。
當然,除此之外,泛型的主要優點還包括了:
增加程式的可塑性 (Flexibility)
重複使用性 (Reusability)
編輯器的型別安全性 (Type safety)
雖說泛型的加入使得編程順暢許多,但是其實泛型本身還是有所缺陷的。
變型 (Variance)
Variance refers to how subtyping between more complex types relates to subtyping between their components. [wiki]
首先,subtype 指的是子類型:
若 S 為 T 的子類型,我們就可以說 S is a subtype of T
並且可以用 :> 來表明繼承關係 (subtyping relation): T :> S另外,我們可以說:
S is more SPECIFIED than T
T is more GENERIC than S
而變型指的是 繼承關係 在更為複雜的情況下是否能維持 T :> S 的關係。
變型有分成四種:
- 協變 (Covariance) : 保留父子關係 (T :> S=> I(T) :> I(S))
- 逆變 (Contravariance):與協變相反 (T :> S => I(S) :> I(T))
- 雙變 (Bivariance) : 協變、逆變兩者皆可 (T :> S => I(S) ≡ I(T)) (Java 中並不存在)
- 不變 (Invariance) : 兩者皆不可
其中 I(X) 指的就是使用 X 為參數類型的泛型。
這些變型的用途應該很難用文字理解,所以我們就用例子來邊做邊學吧
假設我們是一位天才科學家。
經過多次試驗後,我們成功地將NormalDog 進化成 SmartDog 與 SuperDog:
NormalDog :> SmartDog :> SuperDog
本質上,他們會做的事都一樣 ,只是 IQ 有所不同:
class NormalDog {
int iq;
NormalDog(int iq) {this.iq = iq;}
void eat() {}
void drink() {}
}
class SmartDog extends NormalDog {
SmartDog(int iq) { super(iq); }
}
class SuperDog extends SmartDog {
SuperDog(int iq) { super(iq); }
}
因為我們需要餵食這些狗狗們,所以我就寫了個餵食的方法 (feed):
static void feed(NormalDog dog) {}feed(new NormalDog(...)); // pass
feed(new SmartDog(...)); // pass
feed(new SuperDog(...)); // pass
又因為 NormalDog :> SmartDog :> SuperDog,所以很理所當然地只要是 NormalDog 的子類別都會通過測試。
但是,若我們想要餵食一群呢?
不變 (Invariance)
若要餵食一群的狗,我們就會設計 feedDogs 方法如下:
static void feedDogs(List<NormalDog> dogs) {}feedDogs(normalDogs); // pass
feedDogs(smartDogs); // compile error
feedDogs(superDogs); // compile error
由上的測試結果可得出以下結論:
雖然 NormalDog :> SmartDog :> SuperDog
但是,I(NormalDog) 、I (SmartDog) 與 I (SuperDog) 毫無關係。
這裡的 I 指的是 List
由此可知泛型是一種不變型 (Invariance)。
那麼,我們要如何解決這問題呢?
協變 (Covariance)
由於我們希望 feedDogs 可以接受
List<NormalDog>、List<SmartDog> 與 List<SuperDog>,這表示我們需要 :
T :> S 時 List<T> :> List<S>
這正是 協變 的特性。
以下便是 Java 與 Kotlin 定義 協變 的語法:
List<? extends T> // Java
List<out T> // Kotlin
其中,Java 所用的 「?」被稱為 wildcard,而 Kotlin 的 out 則是 variance annotation。
當然,Kotlin 也有一個與 wildcard 相似的關鍵字元:「*」, 我們稱之為 Star Projection。
Wildcard 的功用就是用來表示一個「未知的型別」。
而 ? extends T 則表示 一個未知但繼承 T 的型別
換句話說
? extends T 定義了這未知類型的繼承上限 (Upper Bound)
另外,當我們只定義 List<?> 時,這會被看成 List<? extends Object>。
所以當我們將 feedDogs 改成:
static void feedDogs(List<? extends NormalDog> dogs) {}feedDogs(normalDogs); // pass
feedDogs(smartDogs); // pass
feedDogs(superDogs); // pass
feedDogs 就可以接受 NormalDog 與其繼承者們。
那麼,協變有什麼限制嗎?
其實,當我們定義出協變 (? extends NormalDog) 時,List 只會知道內部有 NormalDog 的子類別,卻無法得知到底確切是哪個。
List<? extends NormalDog> 是 List<NormalDog>、List<SmartDog> 還是 List<SuperDog> 呢 ?
因為這原因,所以 List<? extends T> 無法寫入除了 null 以外的任何物件。
雖說 List<? extends T> 無法提供寫入功能,但因為 List 確定內部的物件一定是 NormalDog 類型,所以還是可以提供讀取功能。
也因為如此,我們也稱 協變的參數類別 為 Producer。
IQ 排名
接下來,我們想要知道實驗結果,所以需要知道狗狗們的 IQ 排名。
當然,我們可以用內建的 List#sort(Comparator c) 方法,但是我們還是自己來實作一個 sort 方法吧 ~
static <T> void sort(List<T> list, Comparator<T> comparator) {
int minIndex = -1;
for (int i = 0; i < list.size() - 1; i++) {
minIndex = i; // j = i + 1 可減少一次判斷
for (int j = i + 1; j < list.size(); j++) {
// smaller => swap
if (comparator.compare(list.get(minIndex),
list.get(j)) < 0)
{
minIndex = j;
}
}
if (minIndex != i) {
// swap
swap(list, i, minIndex);
}
}
}
這方法是運用 Selection Sort 來進行。
當中的 Comparator 其實是一個介面:
public interface Comparator<T> {
int compare(T o1, T o2);
}
它的實作如下:
Comparator<T> comparator = new Comparator<T>() {
@Override
public int compare(T o1, T o2) {
return o2.xxx - o1.xxx;
}
};
整個邏輯是:
只要 前者(o1) 比 後者 (o2) 大,o1 就會被放置在 o2 後面。
由於 泛型 本身是 Invariance,所以我們只能如此使用:
sort(normalDogs, normalDogComparator);
sort(smartDogs, smartDogComparator);
sort(superDogs, superDogComparator);
這時你可能想,我們是否能用一個共用的 Comparator 呢?
第一個想法當然就是使用 Comparator<? extends T> 了。
但是,下面這行卻會出現 compile error:
這是由於,int compare(? extends T o1, ? extends T o2) 中的 o1, o2 是 協變 所以無法接收 T 的類型。
那我們該怎麼辦呢?
我們需要一個可以寫入,但卻能夠接受 NormalDog、SmartDog 與 SuperDog 類別的變型。
逆變 (Contravariance)
定義 : T :> S => I (T) <: I(S)
不同於協變,逆變用的關鍵字是 super 與 in,而使用方法如下:
List<? super T> // Java
List<in T> // Kotlin
這行的意思是
List 所包含的類型是 某未知 且 是被 T 類型繼承的類型
譬如,當 T 是 NormalDog 時,S 可以是 SmartDog 與 SuperDog:
List<? super NormalDog> suDs = new ArrayList<SuperDog>(); // failed
List<? super NormalDog> smDs = new ArrayList<SmartDog>(); // failed
List<? super NormalDog> nDs = new ArrayList<NormalDog>(); // pass
根據 逆變 的定義:
NormalDog :> SmartDog , 因此 List<SmartDog> :> List<NormalDog>
因為我們不可能將父類 (List<SmartDog>)別指向子類別 (List<NormalDog>),這是將物件由 Generic 至 Specific,就如同:
NormalDog dog = new SmartDog(1); // pass
SmartDog nDog = new NormalDog(1); // failed
因此,List<? super NormalDog> 只能接受 List<NormalDog> 和 List<Object>。
所以,逆變其實是給型別定義了一個 下限類別 (Lower Bound)
List<? super SuperDog> suDs = new ArrayList<SuperDog>(); // pass
List<? super SuperDog> smDs = new ArrayList<SmartDog>(); // pass
List<? super SuperDog> nDs = new ArrayList<NormalDog>(); // pass
那,逆變 又有哪些限制呢?
由於 List<? super SmartDog> 只知道內部放的是 某個未知 且 被 SmartDog 繼承的類別。
所以,I<? super T> 無法被讀取。 但是,卻可以被寫入,只是被寫入的資料必須要繼承 T 的型別。
也因為如此,所以 逆變 也被稱為 Consumer。
既然我們知道 逆變 可以被寫入,且必須繼承 T 類型。
因此,我們可以改寫 sort 中的 Comparator 為:
static <T> void sort(List<T> list, Comparator<? super T> comparator)
而如果我們希望全部狗狗都用相同的 Comparator,我們的 T 自然是 NormalDog 了,因為 NormalDog 是狗狗共同繼承的類別:
Comparator<NormalDog> superDogComparator =
new Comparator<NormalDog>() {
@Override
public int compare(NormalDog o1, NormalDog o2) {
return o2.iq - o1.iq;
}
};
口訣
我們可以用一句話將 Java 變型所用的語法記起來:
PECS
Producer Extends Consumer Super
Kotin 中雖然沒有,但是我們還是可以寫一個出來的:
POCI
Producer Out Consumer In
貨出得去,人進得來
補充
最後我想補充一點,雖然 Producer 是 Read-only 而 Consumer 是 Write-only,但是對 Kotlin 而言,這些都可以用 annotation @UnsafeVariance 來解決:
open 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
open fun setT(t: T) { _t = t }
open fun setO(o: @UnsafeVariance O) { _o = o }
open fun setI(i: I) { _i = i }
}
總結
我們在這篇學到在 Java 與 Kotlin 中,變型有三種:
Invariance
- 一般泛型
- 使用固定類型,所以彈性或可塑性不夠
Covariance (aka Producer)
- Java 用 extends ; Kotlin 用 out
- T :> S ; I(T) :> I(S)
- 只可以以 T 型別讀取,無法寫入。
Contravariance (aka Consumer)
- Java 用 super; Kotlin 用 in
- T :> S ; I(T) <: I(S)
- 只可寫入T 的繼承者,無法讀取。
他們看似相似但卻用途不同。
當要選擇合適的變型時,就可以使用
- Java 口訣 PECS 或
- Kotlin 口訣 POCI (貨出得去,人進得來)
來為你選擇需要的變型。
有不明白的或是有寫錯的,歡迎留言指教。
如果這篇文章對您有幫助,那就請您分享和給我點掌聲吧 ~~
後序
看完這篇後,你應該對變型多少有點了解了。
現在,你雖然知道 Java 與 Kotlin 用來定義變型的關鍵字有所差異,但差異點卻不止於此。如果想要多暸解,那就看看:
另外,你可否有想過 泛型 本身是在 Java 5 才導入 Java,那麼 JVM 是如何做到向下相容 (Backward Compatible) 呢?
其中的關鍵行為是 Type Erasure。如果想知道更多關於 Type Erasure 以及如何避面被 Erase 掉的話 (reifiable),請看看: