Java 的類型系統在編譯期與類別載入期 (Bytecode Verifier) 都嚴格保證型別安全,但有一個著名的「逃逸漏洞」——陣列協變,會導致類型錯誤延遲到執行期才拋出 ArrayStoreException。本文透過 ASM 字節碼生成與原始碼雙重視角,深度剖析此機制成因、後果及對泛型設計的深遠影響。
以下代碼完美合法,編譯零錯誤,JVM 驗證器也放行:
public class ArrayCovarianceDemo {
public static void main(String[] args) {
Object[] arr = new Float[1]; // ① 協變賦值:Float[] → Object[]
arr[0] = "a string"; // ② 編譯器以為 Object[] 可存任何 Object
arr[0].toString(); // ③ 執行時拋出 ArrayStoreException
}
}
輸出:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at ArrayCovarianceDemo.main(ArrayCovarianceDemo.java:4)
關鍵指令序列:
iconst_1 // int 1
anewarray java/lang/Float // 建立 Float[1] → 型別 [Ljava/lang/Float;
checkcast [Ljava/lang/Object; // 強制轉型為 [Ljava/lang/Object; (協變賦值)
astore_0 // 存入區域變數 0
aload_0 // 載入陣列引用
iconst_0 // 索引 0
ldc "a string" // 推入 String 常量
aastore // **關鍵:陣列存值指令**
aload_0
iconst_0
aaload // 陣列取值
invokevirtual java/lang/Object.toString()V
return
aastore (陣列存引用) 指令語義:運行時檢查「被存入元素的實際類型」是否為「陣列實際元件類型」的子類型。此處陣列實際類型為 Float[],元件類型 Float,存入 String 失敗,拋出 ArrayStoreException。
歷史包袱:Java 1.0 無泛型,為了寫出通用的陣列操作方法 (如 sort(Object[])),必須允許 String[] 傳入 Object[] 參數。James Gosling 團隊將其視為「速成妥協」,Martin Odersky (Scala 作者、Java 泛型設計核心成員) 直言:
"It was sort of like an original sin. They did something very quickly and thought it was a quick hack. But it actually ruined every design decision later on."
這個決策直接導致:
new List<String>[10] 編譯錯誤,因泛型擦除後運行時類型資訊丟失,無法執行 aastore 的動態檢查。List[],失去型別安全。aastore 都需動態型別檢查 (JVM: aastore / CLI: stelem.ref)。C# / CLI 同樣設計陣列為協變 (string[] 是 object[] 子類型),同樣需運行時檢查,同樣阻礙了泛型陣列的實作。Anders Hejlsberg 後來在 C# 4.0 引入「協變/逆變」修飾符 (out/in) 修補介面/委派協變,但陣列協變仍保留向後相容。
Scala 不支援陣列協變。Array[String] 不是 Array[AnyRef] 子類型。取而代之,Scala 陣列是不變,但提供隱式轉換到 scala.collection.mutable.WrappedArray (支援協變) 及泛型集合庫,並在編譯期透過型別參數化實現類型安全的泛型陣列建立 (new Array[T](10))。
Object[] 或父類型陣列變數。Arrays.copyOf() 或 clone(),防止呼叫端修改內部狀態觸發異常。void foo(Object... args) 實際接收 Object[],傳入 String[] 同樣面臨協變風險。Java 陣列協變是為了無泛型時代的通用性而做的妥協,卻在靜態類型系統上打了一個「洞」,強迫運行時承擔動態檢查成本,並嚴重制約了後續泛型系統的設計。理解這一歷史殘留,有助於我們在日常開發中做出更安全的類型選擇,也能更深刻體會現代語言 (Kotlin, Scala, Rust) 為何堅持陣列/集合不變性與泛型一體化設計。