Java編程思想__泛型(三)

  • 下面是使用模板的C++示例, 你將注意到用於參數化類型的語法十分相似,因爲Java是受C++的啓發。
template<class T> class Manipulator{
    T obj;
 public:
    Manipulator(T x){ obj=x; }
    void maniput(){ obj.f(); }       
}

class HasF{
    public:
        void f(){
        cout << "HasF::f()" << endl;
    }
    
    int main(){
        HasF hf;
        Manipulator<HasF> manipulator(hf);
        manipulator.maniput();
    }
}
  1.  Manipulator 類存儲了一個 T 的對象,有意思的地方是 manipulator() 方法,它在調用 obj 上調用方法 f()。它怎麼能知道 f() 方法是爲類型參數 T 而存在的呢?當你實例化這個模板時,C++編譯器將進行檢查,因此在 Manipulator<HasF> 被實例化的這一刻,它看到 HasF擁有一個方法 f() 。如果情況並非如此,就回到得到一個編譯期錯誤,這樣類型安全就得到了保障。
  2. C++ 編寫這種代碼很簡單,因爲當模板被實例化時,模板代碼知道其模板參數的類型。Java泛型就不同了。如下 Java版本 HasF
class HasF{
    void f(){
        System.out.println("HasF f()");
    }
}

public class Manipulator<T>{
    private T t;

    public Manipulator(T t) {
        this.t = t;
    }
    public void manipultor(){
        //error: 找不到符號方法f()
        t.f();
    }

    public static void main(String[] args) {
        HasF hasF = new HasF();
        Manipulator<HasF> hasFManipulator = new Manipulator<HasF>(hasF);
        hasFManipulator.manipultor();
    }
}
  1. 由於有了擦除,Java編譯器無法將 manupulator() 必須能夠在 t 上調用f() 這一需求映射到 HasF 擁有 f() 這一事實上。
  2. 爲了調用 f() ,我們必須協助泛型類,給定泛型類的邊界,以告知編譯器只能接受遵循這個邊界的類型。這裏衝用了 extends 關鍵字。 由於有了邊界,西面代碼就可以編譯了。
public class Manipulator2<T extends HasF>{
    private T t;

    public Manipulator(T t) {
        this.t = t;
    }
    public void manipultor(){
        t.f();
    }
}
  1. 邊界<T extends HasF> 聲明 T 必須具有類型 HasF 或者從 HasF 導出的類型。如果情況確實如此,那麼就可以安全地在 obj上調用 f()。
  2. 我們說泛型類型參數將擦除到它的第一個邊界(它可能會有多個邊界,稍後你就會看到),我們還提到了類型參數的擦除。編譯器實際上會把類型參數替換爲它的擦除,就像上面的示例一樣。T 擦除到了 HasF,就好像在類的聲明中用 HasF 替換了T一樣。
  3. 你可能已經正確地觀察到 ,在Manipulator2類中,泛型沒有貢獻任何好處。只需很容易地自己去執行擦除,就可以創建出沒有泛型的類:
public class Manipulator3{
    private HasF hasF;

    public Manipulator3(HasF hasF) {
        this.hasF = hasF;
    }
    public void manipultor() {
        hasF.f();
    }
}
  1. 這提出可很重要的一點: 只有當你希望使用的類型參數比某個具體類型(以及它的所有子類型) 更加 泛化 時___也就是說,當你希望代碼能夠跨多個類工作時,使用泛型纔有所幫助。
  2. 因此,類型參數和它們在有用的泛型代碼中的應用,通常比簡單的類替換要更復雜。但是,不能因此而認爲<T extends HashF> 形式是有所缺陷的。
  3. 例如,如果某個類有一個返回T 的方法,那麼泛型就有所幫助,因爲它們之後將返回準確的類型:
class ReturnGenericType<T extends HasF>{
    private T t;

    public ReturnGenericType(T t) {
        this.t = t;
    }
    public T getT(){
        return t;
    }
}
  1. 必須查看所有的代碼,並確定它是否 足夠複雜 到必須使用泛型的程序。

 

遷移兼容性

  • 爲了減少潛在的關於擦除的混淆,你必須清除地認識到這不是一個語言特性。它是Java 的泛型實現中的一個折中,因爲泛型不是Java語言出現時就有的組成部分,所以這種折中是必須的。這種折中會讓你痛苦,因爲你需要習慣它並瞭解爲什麼它會是這樣。
  • 如果泛型在Java1.0中就已經是其一部分了,那麼這個特性將不會擦除來實現___它將使用具體化,使用類型參數保持爲第一類實體,因此你就能夠在類型參數上執行基於類型語言操作和反射操作。你將在本章稍後看到,擦除減少了泛型的泛化性。泛型在Java中仍舊是有用的,只是不如它們設想的那麼有用,而原因就是擦除
  • 在基於擦除的實現中,泛型類型被當做第二類類型處理,即不能再某些重要的上下文環境中使用的類型。泛型類型只有在靜態類型檢查期間纔出現,在此之後,程序中的所有泛型類型都將被擦除,替換爲它們的非泛型上界。例如 List<T> 這樣的類型註解將被擦除爲 List, 而普通的類型變量在未指定邊界的情況下將被擦除爲 Object。
  • 擦除的核心動機它使得泛化的客戶端可以用非泛化的類庫來使用,反之亦然,這經常被稱爲 遷移兼容性。在理想狀態下,當所有事物都可以同時被泛化時,我們就可以專注於此。在現實中,即使程序員只編寫泛型代碼,他們也必須處理在Java SE5之前編寫的非泛型類庫。那些類庫的作者可能從沒有想過要泛化它們的代碼,或者可能剛剛開始接觸泛型。
  • 因此Java泛型不僅必須支持向後兼容性,即現有的代碼和類文件仍舊合法,並且繼續保持其之前的含義。而且還要支持遷移兼容性,使得類庫按照他們自己的步調變爲泛型的,並且當某個類庫變爲泛型時,不會破壞依賴於它的代碼和應用程序。在決定這就是目標之後,Java設計者們和從事此問題相關工作的各個團隊決策認爲擦除是唯一可行的解決方案。通過允許非泛型代碼與泛型代碼共存,擦除使得這種向着泛型的遷移稱爲可能。
  • 例如, 假設某個應用程序具有兩個類庫 X 和 Y,並且 Y 還要使用類庫 Z。隨着JavaSE5 的出現,這個應用程序和這些類庫的創建者最終可能希望遷移到泛型上。但是,當進行這種遷移時,他們有着不同動機和限制。爲了實現遷移兼容性,每個類庫和應用程序都必須與其他所有的部分是否使用了泛型無關。這樣,它們必須不具備探測其他類庫是否使用了泛型的能力。因此,某個特定的類庫使用了泛型這樣的證據必須被擦除。
  • 如果沒有某種類型的遷移途徑,所有已經構建了很長時間的類庫就需要與希望遷移到Java泛型上的開發者說再見了。但是,類庫是編程語言無可爭議的一部分,它們對生成效率會產生最重要的影響,因此這不是一種可以接受的代價。擦除是否是最佳的或者唯一的遷移途徑,還需要時間來證明。

 

擦除的問題

  • 因此,擦除主要的正當理由是從非泛型代碼到泛型代碼的轉變過程,以及在不破壞現有類庫的情況下,將泛型融入Java語言。

  • 擦除使得現有的非泛型客戶端代碼能夠在不改變的情況下繼續使用,直至客戶端準備好使用泛型重寫這些代碼。這是一個崇高的動機,因爲它不會突然間破壞所有現有的代碼。

  • 擦除的代價是顯著的。泛型不能用於顯式地引用運行時類型的操作之中,例如轉型, instanceof 操作和new表達式。因爲所有關於參數的類型信息都丟失了,無論何時,當你在編寫泛型代碼時,必須時刻提醒自己,你只是看起來好像擁有有關參數的類型信息而已。

  • 因此,如果你便攜了下面這樣的代碼段:

class Foo<T>{
    T var;
}
  1. 那麼,看起來當你在創建Foo 的實例時:
Foo<Cat> f=new Foo<Cat>();
  1. class Foo 中的代碼應該知道現在工作於Cat之上,而泛型語法也在強烈暗示: 在整個類中的各個地方,類型 T 都在被替換。但事實並非如此,無論何時,當你在編寫這個類代碼時,必須提醒自己:  不 ,它只是一個 Object
  2. Java 中的泛型基本都是在編譯器這個層次來實現的,在生成的 Java 字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器的時候去掉。這個過程就稱爲 類型擦除
public class ArrayList1{
    
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("str");
        List<Integer> integers = new ArrayList<>();
        integers.add(123);

        System.out.println(list.getClass());
        System.out.println(integers.getClass());
        System.out.println(integers.getClass() == list.getClass());
    }
}

//運行結果爲
true

 

  1. 我們定義兩個ArrayList數組 ,不過一個是 List<String> 泛型類型,只能保存字符串 ,一個是List<Integer> 類型,只能保存 整型。
  2. 我們通過 list 和 integers 對象的getClass() 獲取它們的類的信息,最後結果發現爲 true。說明了泛型類型 String 和Integer 都被擦除掉了,只剩下原始類型。
class ArrayList2{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        //利用反射進行添加數據asd
        list.getClass().getMethod("add",Object.class).invoke(list,"asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

//運行結果爲
1
asd
  1. 在程序中定義一個 List<Integer> 的集合,如果直接調用 add方法,那麼只能存儲 整型數值。
  2. 不過我們利用反射調用 add 方法的時候,卻可以存儲字符串。這就說明了 Integer 泛型實例在編譯之後就被擦除了,只保留了原始類型。

 

類型擦除後保留的原始類型

  • 原始類型(raw type) : 擦除了泛型信息,最後在字節碼中的類型變量的真正類型
  • 無論何時定義一個泛型,相應的原始類型都會被自動地提供。類型變量被擦除,並使用其限定類型(無限的變量用Object)替換。
class Person<T>{

    private T t;

    public T getT() {
        return t;
    }

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

//原始類型爲

class Person{

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setT(Object object) {
        this.object = object;
    }
}
  • 在 Person<T> 中,T 是一個無限定的類型變量,所以用 Object 替換。其結果就是一個普通的類,如同泛型加入 Java 編程語言之前已經實現的那樣。
  • 在程序中可以包含不同類型的 Person,(Person<String> , Person<Integer>) 但是擦除類型後它們就成爲原始的 Person類型了,原始類型都是Object。
  • 如果類型變量有限,那麼原始類型就用第一個邊界的類型變量來替換。例如
class ArrayList3<T extends ArrayList2 & Serializable>{
    //...
}
  1. 那麼原始類型就不是 Object 而是 ArrayList2。
  2. 注意: 如果 ArrayList 這樣聲明 class ArrayList<T extends ArrayList2 & Serializable> ,那麼原始類型就用 Serializable 替換, 而編譯器在必要的時要向 ArrayList2 插入強制類型轉換。爲了提高效率,應該將標籤接口放在邊界限定列表的末尾。

 

要區分原始類型泛型類型變量的類型

  • 在調用泛型方法的時候,我們可以指定泛型,也可以不指定泛型。
  • 在不指定泛型的情況下,泛型變量的類型爲該方法中的幾種參數變量類型的同一個父級的最小級,直到 Object。
  • 在指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。
class ArrayList4{

    //這是一個簡單的泛型方法
    static <T> T add(T t,T k){
        return k;
    }

    public static void main(String[] args) {
        //不指定泛型的時候

        //兩個Integer 所以T爲 Integer
        Integer add = ArrayList4.add(1, 2);
        //一個Integer 一個Double T取同一父級最小級 Number
        Number number = ArrayList4.add(1, 1.2);
        //一個Integer 一格String T取同一父級爲 Serializable
        Serializable ask = ArrayList4.add(1, "ask");

        //指定泛型的時候

        //指定了Integer 所以只能爲 Integer 類型 或其子類 當你傳入 Double 是就會編譯出錯
        Integer add1 = ArrayList4.<Integer>add(1, 2);
        //指定爲 Number 所以可以爲 Integer 和Double
        Number number1 = ArrayList4.<Number>add(1, 2.2);
    }
}
  1. 其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候類型爲 Object。比如ArrayList 中如果不指定泛型類型,那麼這個ArrayList會默認爲 Object 類型可以放置任意類型的對象。
class ArrayList5{
    public static void main(String[] args) {
        ArrayList<Object> objects = new ArrayList<>();
        objects.add(1);
        objects.add("123");
        objects.add(new Date());
    }
}

 

類型擦除引起的問題及解決辦法

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

 

1).先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題

 

  • 既然說類型變量在編譯的時候擦除掉,那爲什麼我們往 List<String> str=new ArrayList<>(); 所創建的數組列表 str 中,不能使用 add 方法添加 整型數據呢? 不是說泛型變量 Integer 會在編譯時候擦除變爲 原始類型 Object ? 爲什麼不能存在別的類型呢? 既然類型擦除了 ,如何保證我們只能使用泛型變量限定的類型呢?
  • Java 是如何解決這個問題的呢?  Java編譯器是通過先檢查代碼中泛型的類型,然後在進行類型擦除,在進行編譯的。
    public static void main(String[] args) {
        List<String> objects = new ArrayList<>();
        objects.add("123");
        //編譯失敗
        objects.add(1);
    }
  1. 使用 add方法添加一個整型, 在 idea中,直接就會報錯,說明這就是在編譯之前的檢查。因爲如果是在編譯之後檢查,類型擦除後,原始類型爲 Obejct ,是應該運行任意引用類型的添加的。可實際上卻不是這樣,這恰恰說明了關於泛型變量的使用,是會在編譯之前檢查的。
  2. 那麼,這麼類型檢查是針對誰的呢? 我們先看看參數化類型與原始類型的兼容
//以前寫法
List list=new ArrayList();

//現在寫法
List<String> lists=new ArrayList<String>();

//如果是與以前的代碼兼容,各種引用傳值之間,必然會出現如下情況

List<String> list1=new ArrayList<>();

和

List list2=new ArrayList<Stering>();
  1. 這樣是沒有錯誤的,不過會有一個編譯時警告。
  2. 不過在 list1 可以實現與完全使用泛型參數一樣的效果, list2 則完全沒有效果。
  3. 因爲,本來類型檢查就是編譯時完成的。new ArrayList() 只是在內存中開闢一塊存儲空間,可以存儲任何的類型對象。而真正涉及及類型檢查的是它的引用,因爲我們使用它引用 list1 來調用它的方法,比如說它調用 add() 方法。所以 list1 引用能完成泛型的類型檢查。
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        list1.add("1234");
        //返回類型爲 String
        String s = list1.get(0);
        
        List list2=new ArrayList<String>();
        list2.add(112);
        list2.add("str");
        list2.add(new Date());
        //返回類型爲 object
        Object o = list2.get(0);
    }
  1. 通過上面的例子,我們可以明白,類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢查,而無關它真正引用的對象。

 

從這裏,我們可以在討論下,泛型中參數化類型爲什麼不考慮繼承關係

List<String> str1=new ArrayList<Object>(); //編譯失敗

List<Object> str2=new ArrayList<String>(); //編譯失敗


//我們先看第一種情況,將第一種情況擴展成下面的形式:

List<Object> objs=new ArrayList<>();
objs.add(new Object());
objs.add(new Object());

List<String> str3=objs; //編譯失敗


//第二種情況
List<String> str4=new ArrayList<>();
str4.add(new String());
str4.add(new String());

List<Object> objts= str4;
  • 實際上,在運行 List<String> str=objs 代碼的時候,就會有編譯錯誤。那麼我們先假設它編譯沒錯。那麼當我們使用 str3 引用 get() 方法取值的時候,返回的都是 String 類型的對象(上面提到了,類型監測是根據引用來決定的),可是它裏面實際上已經被我們存放了 Object 類型的對象,這樣,就會有 ClassCastException 異常,所以爲了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現的原因,就是爲了解決類型轉換的問題,我們不能違背它的初衷)。
  • List<Object> objts= str4; 這種情況比上一種好的多,最起碼,在我們使用 objts 取值的時候不會出現 ClassCastException ,因爲是從String 轉換爲 Object。可是,這樣做有什麼意義呢?  泛型出現的原因,就是爲了解決類型轉換的問題。我們使用泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以Java 不允許這麼幹。在說,你如果又用 objts 的 add() 添加新對象,那麼取值的時候,我們怎麼知道取出來的是String類型還是Object類型?。

 

2).自動類型轉換

  • 因爲類型擦除的問題,所以所有泛型類型變量最後都會被替換爲原始類型。這樣就引起了一個問題,既然都被替換爲原始類型,那麼我們再獲取的時候,不需要進行強制類型轉換呢?
//arrayList 源碼

  /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
  • 可以看到,在return 之前會根據泛型變量進行強轉。
//寫了個簡單的程序

public class ArrayList5 {

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        list1.add("1234");
        String s = list1.get(0);
    }
}

//反編譯如下
public class generic.ArrayList5 {
  public generic.ArrayList5();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: new           #4                  // class java/util/Date
      12: dup
      13: invokespecial #5                  // Method java/util/Date."<init>":()V
      16: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      19: pop
      20: aload_1
      21: iconst_0
      22: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      25: checkcast     #4                  // class java/util/Date
      28: astore_2
      29: return
}
  1. list.get(0) 方法 方法返回值是一個Object 類型 說明類型擦除了 
  2. 然後在 checkcast  #4 操作之後跳轉到 #4  如 new #4 class java/util/Date 是一個Date類型,即做了Date 類型轉換。 所以它不是在get 方法強轉的 是在你調用的地方強轉的。

附一個checkcast解釋

checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:

return ((String)obj);

then the Java compiler will generate something like:

aload_1 ; push -obj- onto the stack
checkcast java/lang/String ; check its a String
areturn ; return it

checkcast is actually a shortand for writing Java code like:

if (! (obj == null || obj instanceof <class>)) {
throw new ClassCastException();
}
// if this point is reached, then object is either null, or an instance of
// <class> or one of its superclasses.

 

3),類型擦除與多態衝突和解決方案

//現在有一個泛型類

public class Pair<T> {
    private T t;

    public T getT() {
        return t;
    }

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

//我們想要一個子類繼承它

public class DataClass extends Pair<Date>{
    @Override
    public Date getT() {
        return super.getT();
    }

    @Override
    public void setT(Date date) {
        super.setT(date);
    }
}
  1. 在這個子類中,我們假設父類泛型類型爲 Pair<Date> 在子類中,我們覆蓋了父類的兩個方法,我們的意願是這個樣的。
  2. 將父類的泛型類型限定爲 Date , 那麼父類裏面的兩個方法的參數都爲 Date 類型。
    public Date getT() {
        return t;
    }

    public void setT(Date t) {
        this.t = t;
    }
  1. 所以,我們再子類中重寫這兩個方法一點問題也沒有,實際上,從它們的 @ Override 標籤中可以看到,一點問題也沒有,實際上是這樣?

分析:

  • 實際上,類型擦除後,父類的泛型類型全部變爲原始類型 Object,所以父類編譯之後會變成下面這樣
public class Pair {
    private Object t;

    public Object getT() {
        return t;
    }

    public void setT(Object t) {
        this.t = t;
    }
}
  1. 再看子類兩個重寫的方法類型:
    @Override
    public Date getT() {
        return super.getT();
    }

    @Override
    public void setT(Date date) {
        super.setT(date);
    }
  1. 先來分析 setT() 方法,父類的類型是 Object ,而子類的類型爲 Date ,參數類型不一樣,如果實在普通的繼承關係中,根本就不會重寫,而是重載。
  2. 我們如下進行測試:
    public static void main(String[] args) {
        DataClass aClass = new DataClass();
        aClass.setT(new Date());
        //編譯錯誤
        aClass.setT(new Object());
    }
  1. 如果是重載,那麼子類中兩個 setT() 方法,一個參數是 Object類型, 一個是參數Date類型,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object類型參數的方法。所以說,確實是重寫了,而不是重載了。
  2. 爲什麼會這樣呢?
  3. 原因: 我們傳入父類的泛型類型是Date ,Pair<Date> 我們的本意是將泛型類變爲如下
class Pair {
	private Date t;
	public Date getValue() {
		return t;
	}
	public void setValue(Date t) {
		this.t= t;
	}
}
  1. 然後,我們重寫參數類型爲 Date的那兩個方法,實現繼承中的多態。
  2. 可是由於種種原因,虛擬機並不能將泛型類型變爲 Date ,只能將類型擦除掉,變爲原始類型Object 。這樣,我們的本意識進行重寫,實現多態。可是類型擦除後,只能變爲重載。這樣,類型擦除就和多態有了衝突。
  3. JVM 知道你的本意? 知道!!! 可是它能直接實現? 不能!!! 如果真的不能的話,那我們怎麼去重寫我們想要的Date 類型的方法呢?
  • JVM 採用了一種特殊的方法,來完成這項功能,那就是橋方法。
  • 首先我們 javap -c className 的方式反編譯 DateClasss 子類的字節碼如下
public class generic.DataClass extends generic.Pair<java.util.Date> {
  public generic.DataClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method generic/Pair."<init>":()V
       4: return

  public java.util.Date getT();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method generic/Pair.getT:()Ljava/lang/Object;
       4: checkcast     #3                  // class java/util/Date
       7: areturn

  public void setT(java.util.Date);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #4                  // Method generic/Pair.setT:(Ljava/lang/Object;)V
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #5                  // class generic/DataClass
       3: dup
       4: invokespecial #6                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: new           #3                  // class java/util/Date
      12: dup
      13: invokespecial #7                  // Method java/util/Date."<init>":()V
      16: invokevirtual #8                  // Method setT:(Ljava/util/Date;)V
      19: return

  public void setT(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #3                  // class java/util/Date
       5: invokevirtual #8                  // Method setT:(Ljava/util/Date;)V
       8: return

  public java.lang.Object getT();
    Code:
       0: aload_0
       1: invokevirtual #9                  // Method getT:()Ljava/util/Date;
       4: areturn
}
  1. 從編譯結果來看,我們本意是重寫 setT 和 getT 方法的子類,竟然有4個方法,其實不用驚奇,最後兩個方法,就是編譯器自己生成的橋方法
  2. 可以看到 橋方法 的參數類型都是 Object ,也就是說,子類中真正覆蓋父類兩個方法就是這兩個我們看不到的橋方法。
  3. 而打在我們自己定義的 setT 和 getT 方法上面的 @Overrride  只不過是假象。而橋方法的內部實現,就只是調用我們自己重寫的那兩個方法。
  4. 所以,Java 虛擬機巧妙地使用了橋方法,來解決了類型擦除和多態的衝突
  5. 不過要提到一點,這裏面的 setT 和 getT 這兩個橋方法的意義又有不同。
  • setT 方法是爲了解決類型擦除與多態之間的衝突。
  • getT 方法是爲了解決類型擦除與多態之間的衝突。

 

  //那麼父類的setT 方法如下
  public Object getT() {
        return t;
    }


//子類重寫如下
    @Override
    public Date getT() {
        return super.getT();
    }
  1. 其實這在普通類繼承中也是普遍存在重寫,這就是協變。
  2. 關於協變......還有有點也許會有疑問,子類中的 橋方法 Object getT() 和 Date getT() 是同時存在的,可是如果是常規的兩個方法,它們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。
  3. 如果使我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因爲虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器爲了實現泛型的多態允許自己做這個看起來 不合法的事情,然後交給虛擬器去區別。

 

4),泛型類型變量不能是基本數據類型

  • 不能用類型參數替換基本類型,就比如 ,沒有 ArrayList<double> ,只有ArrayList<Double> 。
  • 因爲當類型擦除後,ArrayList的原始類型變爲Object ,但是Object類型不能存儲double,只能引用Double的值。

 

5),運行時類型查詢

List<String> str=new ArrayList<String>();
  1. 當類型擦除之後, ArrayList<>(); 只剩下原始類型,泛型String 就不存在了。
  2. 那麼再進行類型查詢的時候使用下面方法是錯誤的
if (str instanceof ArrayList<String>){}
  1. Java限定了這種類型查詢的方式
 if (str instanceof ArrayList<?>){}
  1. ? 是通配符的形式

 

6),異常中使用泛型的問題

  • 不能排除也不能捕獲類的對象。事實上,泛型類擴展 Throwable都不合法。
  • 如下定義將不會通過編譯:
class Problem<T> extends Exception{}
  1. 爲什麼不能擴展 Throwable ,因爲異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉,那麼,假設上面的編譯可行,那麼,在看下面的定義:
try{

}catch(Problem<Integer> e1){
    //...
}catch(Problem<Number> e2){
    //...
}
  1. 類型信息被擦除後,那麼兩個地方的catch都變味原始類型 Object,那麼也就是說,這兩個地方的 catch一模一樣就相當於下面的這樣
try{

}catch(Problem<Object> e1){
    //...
}catch(Problem<Object> e2){
    //...
}
  1. 這個當然就是不行的。就好比,catch兩個一模一樣的普通異常,不能通過編譯一樣。
try{

}catch(Exception e1){
    //...
}catch(Exception e2){  //編譯失敗
    //...
}
  1. 不能在catch 子句中使用泛型變量
public static <T extends Throwable> void doWork(Class<T> t){
        try{
            ...
        }catch(T e){ //編譯錯誤
            ...
        }
   }
  1. 因爲泛型信息在編譯的時候已經變爲原始類型,也就是說上面的T 會變爲原始類型 Throwable,那麼如果可以在 catch子句中使用泛型變量,那麼,下面的定義呢:
public static <T extends Throwable> void doWork(Class<T> t){
        try{
            ...
        }catch(T e){ //編譯錯誤
            ...
        }catch(IndexOutOfBounds e){
        }                         
 }
  1. 根據異常捕獲的原則,一定是子類在前面,父類在後面,那麼上面違背了這個原則。
  2. 即使你在使用該靜態方法的使用 T 是 ArrayIndexOutBounds ,在編譯之後還是會變成 Throwable ,ArrayIndexOutBounds 是IndexOutOfBounds 的子類,違背了異常捕獲的原則。所以Java爲了避免這種的情況,禁止在 catch 子句中使用泛型變量。
  3. 但是在異常聲明中可以使用類型變量。下面方法是合法的。
   public static<T extends Throwable> void doWork(T t) throws T{
       try{
           ...
       }catch(Throwable realCause){
           t.initCause(realCause);
           throw t; 
       }
  }
  1. 這個是沒有任何問題的。

 

7),數組(這個不屬於類型擦除引起的問題)

  • 不能聲明參數化類型的數組。如:
Pair<String> [] pairs=new Pair<String>[10];  //error
  1. 這是因爲擦除後, pairs 的類型變爲 Pair[] ,可以轉化成一個Object[]
Object[] objs=pairs;
  1. 數組可以記住自己的元素類型,下面的賦值會拋出一個 ArrayStoreException 異常信息。
objs="hello";
  1. 對於泛型而言,擦除降低了這個機制的效率。下面的賦值可以通過數組存儲的監測,但任然會導致類型錯誤。
objs=new Pair<Employyee>[];
  1. 提示: 如果需要收集參數化類型對象,直接使用 ArrayList : ArrayList<Pair<String>> 最安全且有效。

 

8),泛型類型的實例化

  • 不能實例化泛型類型,如 index = new T(); 會報錯誤。類型擦除會使這個操作做成 new Object()。
//不能建立一個泛型數組 
   public <T> T minMax(T t){
        T t=new T[2]; //error
        //....
    }
  1. 類似的,擦除會使這個方法總是靠一個 Object[2] 數組。但是,可以利用反射構造泛型對象和數組。利用反射,調用 Array.newInstance:
    public static <T extends Comparable> T[] minMax(T[] t){
        return (T[]) Array.newInstance(t.getClass().getComponentType(),2);
    }

//替換掉如下代碼
    Object[] objs=new Object[2];
    return (T[])objs;

 

9),類型擦除後的衝突

  • 當泛型類型被擦除後,創建條件不能產生衝突。如果在 Pair類中添加下面的equals方法。
public class Pair<T> {
    
    public boolean equals(T t) {
        return null;
    }
}
  1. 考慮一個Parir<String> 。從概念上,它有兩個 equals 方法:
  • boolean equals(String);   //從Pair<T> 中定義
  • boolean equals(Object);  //從Object 中繼承
  1. 但是,這只是一種錯覺,實際上,擦除後方法 boolean equals(T) 變味了 boolean equals(Object) 這就是Object.equals方法是衝突的! 當然,補救的辦法是重新命名引發錯誤的方法。
  2. 泛型規範說明提及另一個原則 要支持擦除的轉換,需要強行制一個類或者類型變量不能同時成爲兩個接口的子類,而這兩個子類是同一接口的不同參數化
class Calendar implements Comparable<Calendar>{ ... }

class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...} //ERROR

GregorianCalendar 會實現 Comparable<GregorianCalendar>  和Comparable<Calendar>這是同一接口的不同參數實現。

這一限制與類型擦除的關係並不很明確。非泛型版本
class Calendar implements Comparable{ ... }

class GregorianCalendar extends Calendar implements Comparable{...} //ERROR

是合法的

 

10),泛型在靜態方法和靜態類中的問題

  • 泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數
public class Pair<T> {
    
    private static T t; //編譯失敗

    public static T getT() {//編譯失敗
        return t;
    }

    public static void setT(T t) {//編譯失敗
        this.t = t;
    }
}
  1. 因爲泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。
  2. 對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。

但是要區分下面這一種情況

public class Pair<T> {
    
    public static <T> T setT(T t) {//這個是正確的
        return null;
    }
}
  1. 因爲這是一個泛型方法,在泛型方法中使用的 T 是自己在方法中定義的 T ,而不是泛型類中的 T 。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章