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

你們好,我是二哥呀!html

今天我來給你們講一下,Java 不能實現真正泛型的緣由是什麼?java

本文已同步至 GitHub 《教妹學 Java》專欄,風趣幽默,通俗易懂,對 Java 初學者親切友善(目前已更新 49 篇),內容包括 Java 語法、Java 集合框架、Java 併發編程、Java 虛擬機等核心知識點,歡迎 star。
GitHub 開源地址:github.com/itwanger/jm…
在線閱讀地址:itwanger.gitee.io/jmx-java/#/git

簡單來回顧一下類型擦除,看下面這段代碼。程序員

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 是不一樣的類。github

但因爲類型擦除的緣由,以上代碼是不會編譯經過的——編譯器會提示一個錯誤:編程

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

也就是說,兩個 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 教程地址:pizzacompiler.sourceforge.net/doc/tutoria…

這裏插一下 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;
     }

     A 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 "main" java.lang.ClassCastException: java.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 添加改進的泛型支持。

源碼地址:openjdk.java.net/projects/va…

我是二哥呀!

本文已同步至 GitHub 《教妹學 Java》專欄(目前已更新 49 篇),風趣幽默,通俗易懂,對 Java 初學者親切友善,😘,內容包括 Java 語法、Java 集合框架、Java 併發編程、Java 虛擬機等核心知識點,歡迎 star

相關文章
相關標籤/搜索