java為什麼要用類型擦除實現泛型?

来源:https://www.cnblogs.com/rese-t/archive/2017/12/31/8158870.html
-Advertisement-
Play Games

為什麼需要泛型? 試想你需要一個簡單的容器類,或者說句柄類,比如要存放一個蘋果的籃子,那你可以這樣簡單的實現: 這樣一個簡單的籃子就實現了,但問題是它只能存放蘋果,之後又出現了另外的一大堆水果類,那你就不得不為這些水果類分別實現容器: 然後你發現你其實在做大量的重覆勞動。所以你幻想你的語言編譯器要是 ...


 

為什麼需要泛型?

試想你需要一個簡單的容器類,或者說句柄類,比如要存放一個蘋果的籃子,那你可以這樣簡單的實現:

class Fruit{}
class Apple extends Fruit{}

class Bucket{
    private Apple apple;
   
     public void set(Apple apple){
        this.apple = apple;
    }
   java學習群669823128
    public Apple get(){
        return this.apple;
    }
}

這樣一個簡單的籃子就實現了,但問題是它只能存放蘋果,之後又出現了另外的一大堆水果類,那你就不得不為這些水果類分別實現容器:

class Fruit{}
class Apple extends Fruit{}
class Banana extends Fruit{}
class Orange extends Fruit{}

class BucketApple{
    private Apple apple;

    public void set(Apple apple){
        this.apple = apple;
    }

    public Apple get(){
        return this.apple;
    }
}

class BucketBanana{
    private Banana banana;

    public void set(Banana banana){
        this.banana = banana;
    }

    public Banana get(){
        return this.banana;
    }
}
class BucketOrange{
    private Orange orange;
java學習群669823128
    public void set(Orange orange){
        this.orange = orange;
    }

    public Orange get(){
        return this.orange;
    }
}

然後你發現你其實在做大量的重覆勞動。所以你幻想你的語言編譯器要是支持某一種功能,能夠幫你自動生成這些代碼就好了。

不過在祈求讓編譯器幫你生成這些代碼之前,你突然想到了Object能夠引用任何類型的對象,所以你只要寫一個Object類型的Bucket就可以存放任何類型了。

class Bucket{
    private Object object;

    public void set(Object object){
        this.object = object;
    }

    public Object get(){
        return this.object;
    }
}

但是問題是這種容器的類型丟失了,你不得不在輸出的地方加入類型轉換:

Bucket appleBucket = new Bucket();
bucket.set(new Apple());
Apple apple = (Apple)bucket.get();

而且你無法保證被放入容器的就是Apple,因為Object可以指向任何引用類型。

這個時候你可能又要祈求編譯器來幫你完成這些類型檢查了。

說道這裡,你應該明白了泛型要保證兩件事,第一:我只需要定義一次類,就可以被“任何”類型使用,而不是對每一種類型定義一個類。第二:我的泛型只能保存我指明的類型,而不是放一堆object引用。

實際上很多語言的泛型就是基於以上兩點而實現的,下麵我就將分別介紹c++,java,c# 的泛型是如何實現的。對比的原因是為了說明為什麼它要這麼實現,這樣實現的優點和缺點是什麼。

c++

巨集

大部分人在大學都學過c語言,你一定記得c語言中有一種被稱為巨集的東西,巨集能夠在預編譯期來“替換”代碼。因為c++是相容c的,所以巨集在c++中同樣可以使用。

比如下麵這段代碼:

#define square(x) x*x
int a = square(5);

在預處理期之後,你代碼中所有的 square(5); ,都被替換成了 5*5 。

同理,有人把c++中的模板稱為高級巨集,當我們在c++中定義一個Bucket模板類之後。可以分別去聲明不同類型的模板實現。

#include <iostream>

template<class T> class Bucket{
private:
    T stuff;
public:
    void set(T t){
        this->stuff = t;
    }

    T get(){
        return this->stuff;
    }
};

class Fruit{};
class Apple : public Fruit{};
class Banana : public Fruit{};
class Orange : public Fruit{};
int main() {
    std::cout << "Hello, World!" << std::endl;
    Bucket<Apple> appleBucket;
    appleBucket.set(Apple());
    Bucket<Banana> bananaBucket;
    bananaBucket.set(Banana());
    return 0;
}

而當你在編譯之前,c++的模板會進行展開,變成類似這個樣子:

class Bucket_Apple  {
    private:
       Apple  stuff;
    public:
        void set(Apple  t){
            this->stuff = t;
        }

       Apple  get(){
            return this->stuff;
        }
 };

這樣你就明白為什麼c++的模板能夠實現泛型了吧,因為它幫你生成了不同類型的代碼。

實際上c++的模板又稱為:編譯時多態技術,功能遠比泛型強大。我們常聽到的:“c++元編程”,即所謂的用代碼來生成代碼的技術。就是基於它的模板。

但是你發現這種技術有一個弊端,就是如果我要聲明瞭100個不同類型的水果容器,那它可能會生成100份代碼。那大量使用模板的c++代碼,編譯後的文件將非常大。(猜測某些編譯器可能會做優化處理,這部分我並不是很清楚,歡迎指正。)

java

java的泛型在底層實現上使用了Object引用,也就是我們之前所提到的第二種方式,但是為了防止你往一個Apple的Bucket添加一個Banana。編譯器會先根據你聲明的泛型類型進行靜態類型檢查,然後再進行類型擦出,擦除為Object。而所謂的類型檢查,就是在邊界(對象進入和離開的地方)處,檢查類型是否符合某種約束,簡單的來說包括:

  1. 賦值語句的左右兩邊類型必須相容。
  2. 函數調用的實參與其形參類型必須相容。
  3. return的表達式類型與函數定義的返回值類型必須相容。
  4. 還有多態類型檢查,既向上轉型可以直接通過,但是向下轉型必須強制類型轉換(前提是有繼承關係)
Number n = new Integer(1);
Integer b = (Integer)n;

但是要註意的一點是,編譯器只會檢查繼承關係是否符合。強轉本身如果有問題,在運行時才會發現。所以下麵這行代碼在運行期才會拋異常。

// n is Integer
Double d = (Double)n;

所以你不能在一個ArrayList 中插入一個String對象, 但在運行是列印泛型類的類型卻是一樣的:

ArrayList<Integer> arrayListInt = new ArrayList<Integer>();
ArrayList<String> arrayListString = new ArrayList<String>();
ArrayList arrayList = new ArrayList();
System.out.println(arrayListInt.getClass().getName());
System.out.println(arrayListString.getClass().getName());
System.out.println(arrayList.getClass().getName());
# all print java.util.ArrayList

但這種技術也有一個弊端,就是既然擦成object了,那麼在運行的時候,你根本不能確定這個對象到底是什麼類型,雖然你可以通過編譯器幫你插入的checkcast來獲得此對象的類型。但是你並不能把T真正的當作一個類型使用:比如這條語句在java中是非法的。

// error
T a = new T();

同理,因為都被擦成了Object,你就不能根據類型來做某種區分。比如異常繼承:

// error
try {
} catch (SomeException<Integer> e) {
} catch (SomeException<String> e) {
}

比如重載:

// error
void f(List<T> v);
void f(List<W> v);

還有因為基本類型int並不屬於oop,所以它不能被擦除為Object,那麼java的泛型也不能用於基本類型。

// error
List<int> a;

類型擦出到底指什麼?

首先你要明白一點,一個對象的類型永遠不會被擦出的,比如你用一個Object去引用一個Apple對象,你還是可以獲得到它的類型的。比如用RTTI。

Object object = new Apple();
System.out.println(object.getClass().getName());
# will print Apple

哪怕它是放到泛型里的。

class Bucket<T>{
    private T t;

    public void set(T t){
        this.t = t;
    }

    public T get(){
        return this.t;
    }

    public  void showClass(){
        System.out.println(t.getClass().getName());
    }
}
Bucket<Apple> appleBucket = new Bucket<Apple>();
appleBucket.set(new Apple());
appleBucket.showClass();
# will print Apple too

為啥?因為引用就是一個用來訪問對象的標簽而已,對象一直在堆上放著呢。

所以不要斷章取義認為類型擦出就是把容器內對象的類型擦掉了,所謂的類型擦出,是指容器類 Bucket<Apple> ,對於Apple的類型聲明在編譯期的類型檢查之後被擦掉,變為和 Bucket<Object> 等同效果,也可以說是 Bucket<Apple> 和 Bucket<Banana> 被擦為和 Bucket<Object> 等價,而不是指裡面的對象本身的類型被擦掉!

c#

c#結合了c++的展開和java的代碼共用。

首先在編譯時,c#會將泛型編譯成元數據,即生成.net的IL Assembly代碼。併在CLR運行時,通過JIT(即時編譯), 將IL代碼即時編譯成相應類型的特化代碼。

這樣的好處是既不會像c++那樣生成多份代碼,又不會像java那樣,丟失了泛型的類型。基本做到了兩全其美。

所以總結一下c++,java,c#的泛型。c++的泛型在編譯時完全展開,類型精度高,共用代碼差。java的泛型使用類型擦出,僅在編譯時做類型檢查,在運行時擦出,共用代碼好,但是類型精度不行。c#的泛型使用混合實現方式,在運行時展開,類型精度高,代碼共用不錯。

為什麼java要用類型擦除?

看到這裡你可能會問,為什麼java要用類型擦除這樣的技術來實現泛型,而不是像c#那樣高大上,難道是因為sun的那群人技術水平遠比不上微軟的那群人麽?

原因是為了向後相容。

你去查查歷史就會知道,c#和java在一開始都是不支持泛型的。為了讓一個不支持泛型的語言支持泛型,只有兩條路可以走,要麼以前的非泛型容器保持不變,然後平行的增加一套泛型化的類型。要麼直接把已有的非泛型容器擴展為泛型。不添加任何新的泛型版本。

當時c#從1.1升級到了2.0,代碼並不是很多,而且都在微軟.net的可控範圍,所以選擇了第一種實現方式,其實你可以發現,c#中有兩種寫法,非泛型寫法和泛型寫法:

// 非泛型
ArrayList array = new ArrayList();
// 泛型
List<int> list = new List<int>();

而java的非泛型容器,已經從1.4.2占有到5.0,市面上已經有大量的代碼,不得已選擇了第二種方法。(之所以是從1.4.2開始,是因為java以前連collection都沒有,是一種vector的寫法。),而且有一個更重要的原因就是之前提到的向後相容。所謂的向後相容,是保證1.5的程式在8.0上還可以運行。(當然指的是二進位相容,而非源碼相容。)所以本質上是為了讓非泛型的java程式在後續支持泛型的jvm上還可以運行。

那麼為什麼使用類型擦除就能保持向後相容呢?

在《java編程思想》中講到了這樣一個例子,下麵兩種代碼在編譯成java虛擬機彙編碼是一樣的,所以無論是函數的返回類型是T,還是你自己主動寫強轉,最後都是插入一條checkcast語句而已:

class SimpleHolder{
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
}

SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String)holder.getObj();

class GenericHolder<T>{
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

GenericHolder<String> holder = new GenericHolder<String>();
holder.setObj("Item");
String s = holder.getObj();
aload_1
invokevirtual // Method get: ()Object
checkcast // class java/lang/String
astore_2
return

我形象的理解為,之前非泛型的寫法,編譯成的虛擬機彙編碼塊是A,之後的泛型寫法,只是在A的前面,後面“插入”了其它的彙編碼,而並不會破壞A這個整體。這才算是既把非泛型“擴展為泛型”,又相容了非泛型。

這下你應該理解“java為什麼要用類型擦除實現泛型?”和這樣實現的優劣了吧

java學習群669823128


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本人一直在走.NET技術路線,考慮到後期公司搞JAVA項目,也算是進行技術災備,開始對JAVA技術進行關註。萬事開頭難,也是上來一頭包。沒辦法,頂著上吧。上面開始分給我任務了。就是對後期項目報表進行方案選型。哥們兒花了兩周的時間總算是提供了幾個方案,以供相關人員選擇。特將此次過程整理如下: 一、萬事 ...
  • 一、Spring MVC 驗證 JSR 303 是ajvaEE6 中的一項子規範 ,叫 Bean Validation 用於對javaBean中的欄位進行校驗。 官方的參考實現是: Hibernate Validator ,此實現和 Hibernate ORM 沒有任何關係 //http://hib ...
  • ##importlogginglogging.debug('debug message')logging.info('info message')logging.warning('warning message') # WARNING:root:warning messagelogging.erro ...
  • 註意迭代器和可迭代對象不同#迭代器:1、有iter方法,2、有next方法li=[1,2,3,4,5]d=iter(li) # 等於li.__iter__()print(d) # <list_iteratorobjectat0x00000174316CC3C8>可以通過next方法取出元素。for循 ...
  • 要讀取鍵盤輸入的數據,需要使用輸入流,可以是位元組輸入流,也可以是位元組輸入流轉換後的字元輸入流。 關於鍵盤輸入,有幾點註意的是:(1).鍵盤輸入流為System.in,其返回的是InputStream類型,即位元組流。(2).位元組流讀取鍵盤的輸入時,需要考慮回車符(\r:13)、換行符(\n:10)。( ...
  • 再有兩天就進入2018了,想想還是要準備一下明年的工作方向。回想當初開始學習函數式編程時的主要目的是想設計一套標準API給那些習慣了OOP方式開發商業應用軟體的程式員們,使他們能用一種接近傳統資料庫軟體編程的方式來實現多線程,並行運算,分散式的數據處理應用程式,前提是這種編程方式不需要對函數式編程語 ...
  • 一、Spring簡介 Spring MVC是當前最優秀的 MVC 框架,自從Spring 2.5 版本發佈後,由於支持註解配置,易用性有了大幅度的提高。Spring 3.0 更加完善,實現了對 Struts 2 的超越。現在越來越多的開發團隊選擇了Spring MVC。 1)Spring3 MVC使 ...
  • 除之前的Spring相關包,還有structs2包外,還需要Hibernate的相關包 首先,Spring整合其他持久化層框架的JAR包 spring-orm-4.2.4.RELEASE.jar (整合Hibernate的) 這個JAR包在Spring框架中包含 Hibernate 需要的JAR包 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...