1. 程式人生 > >java泛型 泛型的內部原理:型別擦除以及型別擦除帶來的問題

java泛型 泛型的內部原理:型別擦除以及型別擦除帶來的問題

一、Java泛型的實現方法:型別擦除

前面已經說了,Java的泛型是偽泛型。為什麼說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型資訊都會被擦除掉。正確理解泛型概念的首要前提是理解型別擦出(type erasure)。

Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java位元組碼中是不包含泛型中的型別資訊的。使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉。這個過程就稱為型別擦除。

如在程式碼中定義的List<object>和List<String>等型別,在編譯後都會程式設計List。JVM看到的只是List,而由泛型附加的型別資訊對JVM來說是不可見的。Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法避免在執行時刻出現型別轉換異常的情況。型別擦除也是Java的泛型實現方法與C++模版機制實現方式之間的重要區別。

可以通過兩個簡單的例子,來證明java泛型的型別擦除。

例1、

  1. publicclass Test4 {  
  2.     publicstaticvoid main(String[] args) {  
  3.         ArrayList<String> arrayList1=new ArrayList<String>();  
  4.         arrayList1.add("abc");  
  5.         ArrayList<Integer> arrayList2=new ArrayList<Integer>();  
  6.         arrayList2.add(123);  
  7.         System.out.println(arrayList1.getClass()==arrayList2.getClass());  
  8.     }  
  9. }  
在這個例子中,我們定義了兩個ArrayList陣列,不過一個是ArrayList<String>泛型型別,只能儲存字串。一個是ArrayList<Integer>泛型型別,只能儲存整形。最後,我們通過arrayList1物件和arrayList2物件的getClass方法獲取它們的類的資訊,最後發現結果為true。說明泛型型別String和Integer都被擦除掉了,只剩下了
原始型別

例2、

  1. publicclass Test4 {  
  2.     publicstaticvoid main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {  
  3.         ArrayList<Integer> arrayList3=new ArrayList<Integer>();  
  4.         arrayList3.add(1);//這樣呼叫add方法只能儲存整形,因為泛型型別的例項為Integer
  5.         arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");  
  6.         for (int i=0;i<arrayList3.size();i++) {  
  7.             System.out.println(arrayList3.get(i));  
  8.         }  
  9.     }  
在程式中定義了一個ArrayList泛型型別例項化為Integer的物件,如果直接呼叫add方法,那麼只能儲存整形的資料。不過當我們利用反射呼叫add方法的時候,卻可以儲存字串。這說明了Integer泛型例項在編譯之後被擦除了,只保留了原始型別

二、型別擦除後保留的原始型別

在上面,兩次提到了原始型別,什麼是原始型別?原始型別(raw type)就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別。無論何時定義一個泛型型別,相應的原始型別都會被自動地提供。型別變數被擦除(crased),並使用其限定型別(無限定的變數用Object)替換。

例3:

  1. class Pair<T> {  
  2.     private T value;  
  3.     public T getValue() {  
  4.         return value;  
  5.     }  
  6.     publicvoid setValue(T  value) {  
  7.         this.value = value;  
  8.     }  
  9. }  
Pair<T>的原始型別為:
  1. class Pair {  
  2.     private Object value;  
  3.     public Object getValue() {  
  4.         return value;  
  5.     }  
  6.     publicvoid setValue(Object  value) {  
  7.         this.value = value;  
  8.     }  
  9. }  
因為在Pair<T>中,T是一個無限定的型別變數,所以用Object替換。其結果就是一個普通的類,如同泛型加入java變成語言之前已經實現的那樣。在程式中可以包含不同型別的Pair,如Pair<String>或Pair<Integer>,但是,擦除型別後它們就成為原始的Pair型別了,原始型別都是Object。

從上面的那個例2中,我們也可以明白ArrayList<Integer>被擦除型別後,原始型別也變成了Object,所以通過反射我們就可以儲存字串了。

如果型別變數有限定,那麼原始型別就用第一個邊界的型別變數來替換。

比如Pair這樣宣告

例4:

  1. publicclass Pair<T extends Comparable& Serializable> {  
那麼原始型別就是Comparable

注意:

如果Pair這樣宣告public class Pair<T extends Serializable&Comparable> ,那麼原始型別就用Serializable替換,而編譯器在必要的時要向Comparable插入強制型別轉換。為了提高效率,應該將標籤(tagging)介面(即沒有方法的介面)放在邊界限定列表的末尾。

要區分原始型別和泛型變數的型別

在呼叫泛型方法的時候,可以指定泛型,也可以不指定泛型。

在不指定泛型的情況下,泛型變數的型別為 該方法中的幾種型別的同一個父類的最小級,直到Object。

在指定泛型的時候,該方法中的幾種型別必須是該泛型例項型別或者其子類。

  1. publicclass Test2{  
  2.     publicstaticvoid main(String[] args) {  
  3.         /**不指定泛型的時候*/
  4.         int i=Test2.add(12); //這兩個引數都是Integer,所以T為Integer型別
  5.         Number f=Test2.add(11.2);//這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
  6.         Object o=Test2.add(1"asd");//這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
  7.                 /**指定泛型的時候*/
  8.         int a=Test2.<Integer>add(12);//指定了Integer,所以只能為Integer型別或者其子類
  9.         int b=Test2.<Integer>add(12.2);//編譯錯誤,指定了Integer,不能為Float
  10.         Number c=Test2.<Number>add(12.2); //指定為Number,所以可以為Integer和Float
  11.     }  
  12.     //這是一個簡單的泛型方法
  13.     publicstatic <T> T add(T x,T y){  
  14.         return y;  
  15.     }  
  16. }  


其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型型別為Object,就比如ArrayList中,如果不指定泛型,那麼這個ArrayList中可以放任意型別的物件。

舉例:

  1. publicstaticvoid main(String[] args) {  
  2.         ArrayList arrayList=new ArrayList();  
  3.         arrayList.add(1);  
  4.         arrayList.add("121");  
  5.         arrayList.add(new Date());  
  6.     }  


三、型別擦除引起的問題及解決方法

因為種種原因,Java不能實現真正的泛型,只能使用型別擦除來實現偽泛型,這樣雖然不會有型別膨脹的問題,但是也引起了許多新的問題。所以,Sun對這些問題作出了許多限制,避免我們犯各種錯誤。

1、先檢查,在編譯,以及檢查編譯的物件和引用傳遞的問題

既然說型別變數會在編譯的時候擦除掉,那為什麼我們往ArrayList<String> arrayList=new ArrayList<String>();所建立的陣列列表arrayList中,不能使用add方法新增整形呢?不是說泛型變數Integer會在編譯時候擦除變為原始型別Object嗎,為什麼不能存別的型別呢?既然型別擦除了,如何保證我們只能使用泛型變數限定的型別呢?

java是如何解決這個問題的呢?java編譯器是通過先檢查程式碼中泛型的型別,然後再進行型別擦除,在進行編譯的。

舉個例子說明:

  1. publicstaticvoid main(String[] args) {  
  2.         ArrayList<String> arrayList=new ArrayList<String>();  
  3.         arrayList.add("123");  
  4.         arrayList.add(123);//編譯錯誤
  5.     }  
在上面的程式中,使用add方法新增一個整形,在eclipse中,直接就會報錯,說明這就是在編譯之前的檢查。因為如果是在編譯之後檢查,型別擦除後,原始型別為Object,是應該執行任意引用型別的新增的。可實際上卻不是這樣,這恰恰說明了關於泛型變數的使用,是會在編譯之前檢查的。

那麼,這麼型別檢查是針對誰的呢?我們先看看引數化型別與原始型別的相容

以ArrayList舉例子,以前的寫法:

  1. ArrayList arrayList=new ArrayList();  
現在的寫法:
  1. ArrayList<String>  arrayList=new ArrayList<String>();  

如果是與以前的程式碼相容,各種引用傳值之間,必然會出現如下的情況:
  1. ArrayList<String> arrayList1=new ArrayList(); //第一種 情況
  1. ArrayList arrayList2=new ArrayList<String>();//第二種 情況

這樣是沒有錯誤的,不過會有個編譯時警告。

不過在第一種情況,可以實現與 完全使用泛型引數一樣的效果,第二種則完全沒效果。

因為,本來型別檢查就是編譯時完成的。new ArrayList()只是在記憶體中開闢一個儲存空間,可以儲存任何的型別物件。而真正涉及型別檢查的是它的引用,因為我們是使用它引用arrayList1 來呼叫它的方法,比如說呼叫add()方法。所以arrayList1引用能完成泛型型別的檢查。

而引用arrayList2沒有使用泛型,所以不行。

舉例子:

  1. 相關推薦

    java 內部原理型別除以型別帶來的問題

    一、Java泛型的實現方法:型別擦除 前面已經說了,Java的泛型是偽泛型。為什麼說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型資訊都會被擦除掉。正確理解泛型概念的首要前提是理解型別擦出(type erasure)。 Java中的泛型基本上都是在

    java(二)、內部原理型別除以型別帶來的問題

    參考:java核心技術 一、Java泛型的實現方法:型別擦除 前面已經說了,Java的泛型是偽泛型。為什麼說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型資訊都會被擦除掉。正確理解泛型概念的首要前提是理解型別擦出(type erasure)。 Java中的泛型基本上

    內部原理型別除以型別帶來的問題

    轉載https://www.cnblogs.com/xll1025/p/6489088.html 1泛型擦除 1使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉。這個過程就稱為型別擦除。 2原始型別名稱:刪去型別引數後的泛型型別名 3擦除型別變數後,並替換為限定型別(型別引數

    java 內部原理除以帶來的問題

    st2 往裏面 避免 我們 lar 屬於 util get 驚奇 一、Java泛型的實現方法:類型擦除 前面已經說了,Java的泛型是偽泛型。為什麽說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型信息都會被擦除掉。正確理解泛型概念的首要前提是理解類型擦出(type

    深入探索Java工作原理JVM記憶體回收其他

    Java語言引入了Java虛擬機器,具有跨平臺執行的功能,能夠很好地適應各種Web應用。同時, 為了提高Java語言的效能和健壯性,還引入瞭如垃圾回收機制等新功能,通過這些改進讓 Java具有其獨特的工作原理。 1.Java虛擬機器 Java虛擬機器(Java Virtual

    Java多執行緒(一)執行緒基礎建立

    (一)、執行緒的生命週期 新建狀態: 使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒。 就緒狀態: 當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒

    列舉實現單例原理執行緒安全發序列化依舊為單例原因

    單例的列舉實現在《Effective Java》中有提到,因為其功能完整、使用簡潔、無償地提供了序列化機制、在面對複雜的序列化或者反射攻擊時仍然可以絕對防止多次例項化等優點,單元素的列舉型別被作者認為是實現Singleton的最佳方法。 其實現非常簡單,如下:

    MIPS浮點運算,載入,加 減 乘 除以比較大小

    迭代法求平方根 main: li $v0,6 syscall #輸入浮點數 c.lt.s  $f14,$f10 #if(f14<f10) set condition flag=1 bc1t label#if(condition flag==1)go end label

    Java虛擬機器學習(8)檢視JVM引數值的命令列工具

    檢視JVM各個引數值方式 1. HotSpot vm中的各個globals.hpp檔案  檢視jvm初始的預設值及引數 2.-XX:+PrintFlagsInitial引數 顯示所有可設定引數及預設值,可結

    Java 字串轉化成公式計算 (運算子加+、減-、乘*、/、求餘%)

    今天在牛客網遇到一個題目,圖片如下計算:加+、減-、乘*、除/、求餘%快速尋找到方法:(階乘自己寫吧)ScriptEngine js = new ScriptEngineManager().getEng

    java的一些知識點Java--應用--接口、方法、數組、嵌套

    泛型數組 light inf 返回值 通過 類實例化 this str set 感謝這位大神: http://blog.csdn.net/waldmer/article/details/12773021 1、泛型接口 1.1泛型接口的基本概念 1.2泛型接口實現的兩

    java自定義 面試題接收任意數組進行反轉

    static class ava div 叠代器 類型 應用 system length 不用泛型只能操作某種類型進行反轉 代碼如下: package com.swift.fanxing; import org.junit.Test; public class Ren

    java基礎學習總結(九)深入理解Java

    一、什麼是泛型         “泛型” 意味著編寫的程式碼可以被不同型別的物件所重用。泛型的提出是為了編寫重用性更好的程式碼。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。 比如常見的集合類 LinkedList: publi

    Java語言學習(十一)列舉型別

        Java中一個重要的型別:列舉,它可以用來表示一組取值範圍固定的變數,使用 enum 關鍵字定義列舉型別,其中元素不能重複,通常大寫表示。利用Java的反射機制,可以在執行時分析類,如檢視列舉型別的修飾符、父類和自定義方法等,下面簡單說下。    

    Java程式設計思想 第十五章

    1. 泛型 “泛型”意思就是適用於許多型別。 使用泛型的目的之一: 指定容器持有什麼型別,讓編譯器確保正確性,而不是在執行期發現錯誤。 這個容器可以看成是有其他型別物件作為成員的類,而不單單只是JDK中的容器類。 2.簡單的泛型 2.1 元組

    扒一拔Java 中的(一)

    目錄 1 泛型 1.1 為什麼需要泛型 1.2 型別引數命名規約 2 泛型的簡單實用 2.1 最基本最常用 2.2 簡單泛型類 2.2.1 非泛型類 2.2.2 泛型類的定義 2.2.3 泛型類的使用

    Java 總結(三)萬用字元的使用

    簡介 前兩篇文章介紹了泛型的基本用法、型別擦除以及泛型陣列。在泛型的使用中,還有個重要的東西叫萬用字元,本文介紹萬用字元的使用。 這個系列的另外兩篇文章: Java 泛型總結(一):基本用法與型別擦除 Java 泛型總結(二):泛型與陣列 陣列的協變 在瞭解萬用字

    Java學習筆記——

    ray dem 想想 string stub odt 好處 reac test 一、什麽是泛型?為什麽要使用泛型? 且看代碼: 1 public class GenericsDemo { 2 3 public static void main(String

    Java,通配符和C#對照

    size list ack ace arr 類型通配符 語法 ++ net c#的泛型沒有類型通配符,原因是.net的泛型是CLR支持的泛型,而Java的JVM並不支持泛型,僅僅是語法糖,在編譯器編譯的時候都轉換成object類型 類型通配符在java中表示的是泛型

    Java中的

    產生 range 好的 基於 arr div rdquo nbsp 打印 以下內容引用自http://wiki.jikexueyuan.com/project/java/generics.html: 如果寫一個單獨的能在一個整型數組,一個字符串數組或者一個任何類型支持排序