剖根問底:Java 不能實現真正泛型的原因是什麼?

沉默王二發表於2021-07-30

大家好,我是二哥呀!

今天我來給大家講一下,Java 不能實現真正泛型的原因是什麼?

本文已同步至 GitHub 《教妹學 Java》專欄,風趣幽默,通俗易懂,對 Java 初學者親切友善,麼麼噠?,內容包括 Java 語法、Java 集合框架、Java 併發程式設計、Java 虛擬機器等核心知識點,歡迎 star。

GitHub 開源地址:https://github.com/itwanger/jmx-java
碼雲開源地址:https://gitee.com/itwanger/jmx-java
CodeChina:https://codechina.csdn.net/qing_gee/jmx-java
線上閱讀地址:https://itwanger.gitee.io/jmx-java/#/

簡單來回顧一下型別擦除,看下面這段程式碼。

public class Cmower {
    public static void method(ArrayList<String> list) {
        System.out.println("Arraylist<String> list");
    }

    public static void method(ArrayList<Date> list) {
        System.out.println("Arraylist<Date> list");
    }
}

在淺層的意識上,我們會認為 ArrayList<String> listArrayList<Date> list 是兩種不同的型別,因為 String 和 Date 是不同的類。

但由於型別擦除的原因,以上程式碼是不會編譯通過的——編譯器會提示一個錯誤:

'method(ArrayList)' clashes with 'method(ArrayList)'; both methods have same erasure

也就是說,兩個 method() 方法經過型別擦除後的方法簽名是完全相同的,Java 是不允許這樣做的。

也就是說,按照我們的假設:如果 Java 能夠實現真正意義上的泛型,兩個 method() 方法是可以同時存在的,就好像方法過載一樣。

public class Cmower {
    public static void method(String list) {
    }

    public static void method(Date list) {
    }
}

為什麼 Java 不能實現真正意義上的泛型呢?背後的原因是什麼?

第一,相容性

Java 在 2004 年已經積累了較為豐富的生態,如果把現有的類修改為泛型類,需要讓所有的使用者重新修改原始碼並且編譯,這就會導致 Java 1.4 之前打下的江山可能會完全覆滅。

想象一下,你的程式碼原來執行的好好的,就因為 JDK 的升級,導致所有的原始碼都無法編譯通過並且無法執行,是不是會非常痛苦?

型別擦除就完美實現了相容性,Java 1.5 之後的類可以使用泛型,而 Java 1.4 之前沒有使用泛型的類也可以保留,並且不用做任何修改就能在新版本的 Java 虛擬機器上執行。

老使用者不受影響,新使用者可以自由地選擇使用泛型,可謂一舉兩得。

第二,不是“實現不了”

這部分內容參考自 R大@RednaxelaFX

Pizza,1996 年的實驗語言,在 Java 的基礎上擴充套件了泛型。

Pizza 教程地址:http://pizzacompiler.sourceforge.net/doc/tutorial.html

這裡插一下 Java 的版本歷史,大家好有一個時間線上的觀念。

  • 1995年5月23日,Java語言誕生
  • 1996年1月,JDK1.0 誕生
  • 1997年2月18日,JDK1.1釋出
  • 1998年2月,JDK1.1被下載超過2,000,000次
  • 2000年5月8日,JDK1.3釋出
  • 2000年5月29日,JDK1.4釋出
  • 2004年9月30日18:00 PM,J2SE1.5 釋出

也就是說,Pizza 在 JDK 1.0 的版本上就實現了“真正意義上的”泛型,我引過來兩段例子,大家一看就明白了。

首先是 StoreSomething,一個泛型類,識別符號是大寫字母 A 而不是我們熟悉的大寫字母 T。

class StoreSomething<A{
     A something;

     StoreSomething(A something) {
         this.something = something;
     }

     void set(A something) {
         this.something = something;
     }

     get() {
         return something;
     }
}

這個 A 呢,可以是任何合法的 Java 型別:

StoreSomething<String> a = new StoreSomething("I'm a string!");
StoreSomething<int> b = new StoreSomething(17+4);

b.set(9);

int i = b.get();
String s = a.get();

對吧?這就是我們想要的“真正意義上的泛型”,A 不僅僅可以是引用型別 String,還可以是基本資料型別。要知道,Java 的泛型不允許是基本資料型別,只能是包裝器型別。

除此之外,Pizza 的泛型還可以直接使用 new 關鍵字進行宣告,並且 Pizza 編譯器會從構造方法的引數上推斷出具體的物件型別,究竟是 String 還是 int。要知道,Java 的泛型因為型別擦除的原因,程式設計師是無法知道一個 ArrayList 究竟是 ArrayList<String> 還是 ArrayList<Integer> 的。

ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();

System.out.println(ints.getClass());
System.out.println(strs.getClass());

輸出結果:

class java.util.ArrayList
class java.util.ArrayList

都是 ArrayList 而已。

那 Pizza 這種“真正意義上的泛型”為什麼沒有被 Java 採納呢?這是大家都很關心的問題。

事實上,Java 的核心開發組對 Pizza 的泛型設計非常感興趣,並且與 Pizza 的設計者 Martin 和 Phil 取得了聯絡,新合作了一個專案 Generic Java,爭取在 Java 中新增泛型支援,但不引入 Pizza 的其他功能,比如說函數語言程式設計。

這裡再補充一點維基百科上的資料,Martin Odersky 是一名德國電腦科學家,他和其他人一起設計了 Scala 程式語言,以及 Generic Java(還有之前的 Pizza),他實現的 Generic Java 編譯器成為了 Java 編譯器 javac 的基礎。

站在馬後炮的思維來看,Pizza 的泛型設計和函數語言程式設計非常具有歷史前瞻性。然而 Java 的核心開發組在當時似乎並不想把函數語言程式設計引入到 Java 中。

以至於 Java 在 1.4 之前仍然是不支援泛型的,為什麼 Java 1.5 的時候又突然支援泛型了呢?

當然是到了不支援不行的時候了。

沒有泛型之前,我們可以這樣寫程式碼:

ArrayList list = new ArrayList();
list.add("沉默王二");
list.add(new Date());

不管是 String 型別,還是 Date 型別,都可以一股腦塞進 ArrayList 當中,這看起來似乎很方便,但取的時候就悲劇了。

String s = list.get(1);

這樣取行嗎?

不行。

還得加上強制轉換。

String s = (String) list.get(1);

但我們知道,這行程式碼在執行的時候必然會出錯:

Exception in thread "mainjava.lang.ClassCastExceptionjava.util.Date cannot be cast to java.lang.String

這就又回到“相容性”的問題了。

Java 語言和其他程式語言不一樣,有著沉重的歷史包袱,1.5 之前已經有大量的程式部署在生產環境下了,這時候如果一刀切,原來沒有使用泛型的程式碼直接扼殺了,後果不堪想象。

Java 一直以來都強調相容性,我認為這也是 Java 之所以能被廣泛使用的主要原因之一,開發者不必擔心 Java 版本升級的問題,一個在 JDK 1.4 上可以跑的程式碼,放在 JDK 1.5 上仍然可以跑。

這裡必須得說明一點,J2SE1.5 的釋出,是 Java 語言發展史上的重要里程碑,為了表示該版本的重要性,J2SE1.5 也正式更名為 Java SE 5.0,往後去就是 Java SE 6.0,Java SE 7.0。。。。

但 Java 並不支援高版本 JDK 編譯生成的位元組碼檔案在低版本的 JRE(Java 執行時環境)上跑。

針對泛型,相容性具體表現在什麼地方呢?

ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
ArrayList list;
list = ints;
list = strs;

表現在上面這段程式碼必須得能夠編譯執行。怎麼辦呢?

就只能搞型別擦除了!

真所謂“表面上一套,背後玩另外一套”呀!

編譯前進行泛型檢測,ArrayList<Integer> 只能放 Integer,ArrayList<String> 只能放 String,取的時候就不用擔心型別強轉出錯了。

但編譯後的位元組碼檔案裡,是沒有泛型的,放的都是 Object。

Java 神奇就神奇在這,表面上萬物皆物件,但為了效能上的考量,又存在 int、double 這種原始型別,但原始型別又沒辦法和 Object 相容,於是我們就只能寫 ArrayList<Integer> 這樣很佔用記憶體空間的程式碼。

這恐怕也是 Java 泛型被吐槽的原因之一了。

一個好訊息是 Valhalla 專案正在努力解決這些因為泛型擦除帶來的歷史遺留問題。

Project Valhalla:正在進行當中的 OpenJDK 專案,計劃給未來的 Java 新增改進的泛型支援。

原始碼地址:http://openjdk.java.net/projects/valhalla/


我是二哥呀!

本文已同步至 GitHub 《教妹學 Java》專欄,風趣幽默,通俗易懂,對 Java 初學者親切友善,麼麼噠?,內容包括 Java 語法、Java 集合框架、Java 併發程式設計、Java 虛擬機器等核心知識點,歡迎 star

相關文章