1. 程式人生 > >【系列】重新認識Java——泛型(通配、特性和注意點)

【系列】重新認識Java——泛型(通配、特性和注意點)

上一篇文章介紹了Java泛型中的基礎及原理,本文將繼續研究有關Java泛型的內容。本文的主要內容有:

  1. 泛型的特性
  2. 泛型通配
  3. 泛型類與普通類的不同點,也是日常開發要主要的點

泛型特性

泛型的相容性

首先要強調的是,泛型是編譯時才會檢查合法性,編譯後會直接擦除泛型資訊。正由於這一點,所以在使用Eclipse編寫原始碼時,如果程式碼不合法,它會直接提示我們。Java編譯器是向後相容的,也就是低版本的原始碼可以用高版本編譯器進行編譯。下面來看看那些相容性程式碼。

  1. 引用和例項化都不包含泛型資訊。
import java.util.ArrayList;
import java.util.List;

/**
 * @author
xialei * @version 1.0 2016年6月28日下午9:03:44 */
public class Compatibility { public static void main(String[] args) { // 下面編譯通過 List list1 = new ArrayList(); list1.add("123"); list1.add(1); } }

上面的這段程式碼是可以通過編譯的,這是JDK1.4之前的寫法,所以可以驗證JDK1.5之後的編譯器是可以相容JDK1.4之前的原始碼的。不過,筆者在JDK1.8.x版本的編譯器進行編譯時,會丟擲如下所示的警告資訊。很顯然,如果類被定義成泛型類,但是在實際使用時不使用泛型特性,這是不推薦的做法!

注: Compatibility.java使用了未經檢查或不安全的操作。
注: 有關詳細資訊, 請使用 -Xlint:unchecked 重新編譯。
  1. 引用使用泛型,例項化不使用泛型。
import java.util.ArrayList;
import java.util.List;

/**
 * @author xialei
 * @version 1.0 2016年6月28日下午9:03:44
 */
public class Compatibility {

    public static void main(String[] args) {

        // 編譯不通過
List<String> list2 = new ArrayList(); list2.add("123"); list2.add(1); // 這裡出錯 } }

上面的程式碼編譯不通過,由於對引用使用了泛型,其中的所能容納的物件必須為String 型別。這種寫法實際上跟完整寫法的作用一致,不過Eclipse仍然會警告。

  1. 引用不使用泛型,例項化使用泛型。
import java.util.ArrayList;
import java.util.List;

/**
 * @author xialei
 * @version 1.0 2016年6月28日下午9:03:44
 */
public class Compatibility {

    public static void main(String[] args) {
        // 編譯通過
        List list3 = new ArrayList<String>();
        list3.add("123");
        list3.add(1);
    }
}

上面的這段程式碼可以編譯通過,其效果與1(不使用泛型)完全一致。結合2、3可以知道,編譯時只能做引用的型別檢查,而無法檢查引用所指向物件的實際型別。

泛型與繼承

在使用泛型時,引用的引數型別與實際物件的引數型別要保持一致(萬用字元除外),就算兩個引數型別是繼承關係也是不允許的。看看下面的2行程式碼,它們均不能通過編譯。

ArrayList<String> arrayList1 = new ArrayList<Object>(); //編譯錯誤  
ArrayList<Object> arrayList1 = new ArrayList<String>(); //編譯錯誤  

下面來探討一下為什麼不能這麼做。

  1. 第1種情況,如果這種程式碼可以通過編譯,那麼呼叫get()方法返回的物件應該是String,但它實際上可以存放任意Object型別的物件,這樣在呼叫型別轉換指令時會丟擲ClassCastException。這樣可以不是那麼明顯,來看看下面的程式碼。arrayList1中實際存放的Object物件,所以在進行型別轉換時會丟擲異常。這原本就是泛型想要極力避免的問題,所以Java允許這種寫法。
ArrayList<Object> arrayList1 = new ArrayList<Object>();  
arrayList1.add(new Object());  
ArrayList<String> arrayList2 = arrayList1; //編譯錯誤  
  1. 第2種情況。雖然String型別的物件轉換為Object不會有任何問題,但是這有什麼意義呢?我們原本想要用String物件的方法,但最終將其賦予了一個Object型別的引用。如果需要使用String中的某些方法,必須將Object強制轉換為String。這樣不會丟擲異常,但是卻違背了泛型設計的初衷。

泛型與多型

下面來考慮一下泛型中多型問題。普通型別的多型是通過繼承並重寫父類的方法來實現的,泛型也不例外,下面是一個泛型多型示例。

/*
 * 個人主頁:http://hinylover.space
 *
 * Creation Date: 2016年7月2日 下午7:50:35
 */
package demo.blog.java.generic;

/**
 * @author xialei
 * @version 1.0 2016年7月2日下午7:50:35
 */
public class Father<T> {

    public void set(T t) {
        System.out.println("I am father, t=" + t);
    }

    public T get() {
        return null;
    }
}
/*
 * 個人主頁:http://hinylover.space
 *
 * Creation Date: 2016年7月2日 下午7:50:57
 */
package demo.blog.java.generic;


/**
 * @author xialei
 * @version 1.0 2016年7月2日下午7:50:57
 */
public class Son extends Father<String> {

    @Override
    public void set(String t) {
        super.set(t);
        System.out.println("I am son.");
    }

    @Override
    public String get() {
        return super.get();
    }

    public static void main(String[] args) {
        Father<String> father = new Son();
        father.set("hello world");
    }
}

上面定義了一個泛型父類和一個實際引數為String型別的子類,並“重寫”了set(T)get()方法。Son類中的@Override註解也清楚地顯示這是一個重寫方法,最終執行的結果如下,與想象中的結果完全一致。

I am father, t=hello world
I am son.

真的這麼簡單麼?雖然表面上(原始碼層面)來看,泛型多型與普通類的多型並無二樣,但是其內部的實時原理卻大相徑庭。

從上一篇文章可以知道,泛型類Father在編譯後會擦除泛型資訊,所有的泛型引數都會用Object類替代。實際上,Father編譯後的位元組碼與下面的程式碼完全一致。

public class Father {

    public void set(Object t) {
        System.out.println("I am father, t=" + t);
    }

    public Object get() {
        return null;
    }
}

Son類的與最終會變為:

public class Son extends Father {

    @Override
    public void set(String t) {
        super.set(t);
        System.out.println("I am son.");
    }

    @Override
    public String get() {
        return super.get();
    }

    public static void main(String[] args) {
        Father father = new Son();
        father.set("hello world");
    }
}

FatherSon類的set()方法的引數型別不一樣,所以,這並不是方法重寫,而是方法過載!但是,如果是過載,那麼Son類就應該會繼承Father類的set(Object)方法,也就是Son會同時包含set(String)set(Object),下面來測試一下。

Son son = new Son();
son.set("test");
son.set(new Object()); // 編譯錯誤

當set一個Object物件時,編譯無法通過。這就很奇怪了,感覺跟之前學到的知識是相悖的。我們原本想通過重寫方法來實現多型,但由於泛型的型別擦除,卻最終變成了過載,所以型別擦除與多型有了矛盾。那麼Java是怎麼解決這個問題的呢?還是從位元組碼中找答案吧。Son類最終的編譯結果如下:

{
  public demo.blog.java.generic.Son();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method demo/blog/java/generic/Father."<init>":()V
         4: return
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ldemo/blog/java/generic/Son;

  public void set(java.lang.String);         // 我們重寫的方法
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #16                 // Method demo/blog/java/generic/Father.set:(Ljava/lang/Object;)V
         5: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #25                 // String I am son.
        10: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: return
      LineNumberTable:
        line 17: 0
        line 18: 5
        line 19: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   Ldemo/blog/java/generic/Son;
            0      14     1     t   Ljava/lang/String;

  public java.lang.String get();              // 我們重寫的方法
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #36                 // Method demo/blog/java/generic/Father.get:()Ljava/lang/Object;
         4: checkcast     #39                 // class java/lang/String
         7: areturn
      LineNumberTable:
        line 23: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Ldemo/blog/java/generic/Son;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #1                  // class demo/blog/java/generic/Son
         3: dup
         4: invokespecial #43                 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #44                 // String hello world
        11: invokevirtual #16                 // Method demo/blog/java/generic/Father.set:(Ljava/lang/Object;)V
        14: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            8       7     1 father   Ldemo/blog/java/generic/Father;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8       7     1 father   Ldemo/blog/java/generic/Father<Ljava/lang/String;>;

  public java.lang.Object get();              // 編譯器生成的方法
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #52                 // Method get:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public void set(java.lang.Object);          // 編譯器生成的方法
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #39                 // class java/lang/String
         5: invokevirtual #54                 // Method set:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}

編譯結果有點長,為了方便,省去了無關的位元組碼。先注意到第77行和第90行,這裡面多了一個Object get()方法和set(Object)方法,這兩個方法在Son類原始碼裡面並不存在,這是編譯器為了解決泛型的多型問題而自動生成的方法,稱為“橋方法”。這兩個方法的簽名與Father類中的兩個方法的簽名完全一致,這才是真正的方法重寫。也就是說,子類真正重寫的我們看不到的橋方法,啊,多麼痛的領悟!!!@Override註解只是假象,讓人誤以為他們真的是重寫方法。

再看看set(Object)橋方法的實現細節,在第97行處先將Object物件強制轉換為String物件,然後呼叫Son中的set(String)方法。饒了一個圈,最終才回到我們“重寫”的方法。main方法中原本呼叫父類的set(Object)方法,由於子類通過橋方法重寫了這個方法,所以最終的呼叫順序是:set(Object) -> set(String)

這是腦海中必然會閃現出這麼一個問題,既然呼叫的是set(Object)方法,那麼可以在原始碼中set一個Object物件麼?下面來測試一下:

Father<String> father = new Son();
father.set("hello world");
father.set(new Object()); // 編譯錯誤

上面會出現編譯錯誤,如果是在執行時環境中,這樣是可以執行的,不過會出現ClassCastException異常。

set(Object)橋方法的意義不同,Object get()並不僅僅解決泛型與重寫的衝突,而更具有一般性。看看下面的程式碼,這是一個普通類的繼承,

/**
 * 普通類的繼承。 
 * @author xialei
 * @version 1.0 2016年7月2日下午8:41:47
 */
public class GeneralFather {

    public Object get() {
        return null;
    }
}
/**
 * 普通類的繼承。 
 * @author xialei
 * @version 1.0 2016年7月2日下午8:42:10
 */
public class GeneralSon extends GeneralFather {

    @Override
    public String get() {
        return "";
    }
}

子類的返回型別是父類的返回型別的子類,這是允許的,這種特性叫做Java返回值的協變性。而協變性的實現方法就是上面所述的橋方法。

這裡還會有疑惑,set方法可以通過引數型別來確定呼叫的方法。但是,引數一樣而返回值不一樣是不能過載的。如果我們在原始碼中通過編寫String get()Object get()方法是無法通過編譯的。雖然,編譯器無法通過編譯,但是JVM是可以編寫這兩種方法的,它呼叫方法時,將返回值也作為方法簽名的一部分。有種只許州官放火,不許百姓點燈的感覺。可以看到,JVM做了不少我們認為不合法的事情,所以如果不深入研究底層原理,有些問題根本解釋不了。

泛型通配

所謂泛型通配,是指在宣告泛型型別的變數時,可以不必直接指定具體的泛型,而可以使用萬用字元號來代表一系列型別。萬用字元有2種:

  1. 無邊界萬用字元,用
/*
 * 個人主頁:http://hinylover.space
 *
 * Creation Date: 2016年7月2日 下午9:18:04
 */
package demo.blog.java.generic;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 解釋為什麼需要泛型通配。
 * @author xialei
 * @version 1.0 2016年7月2日下午9:18:04
 */
public class WhyNeedWildcard {

    /**
     * 建立一個能夠列印所有集合物件中的元素的方法。
     * @param collection
     */
    public static void printCollection(Collection<Object> collection) {
        for (Object object : collection) {
            System.out.println(object);
        }
    }

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("t1");
        list.add("t2");

        printCollection(list); // 編譯不通過。The method printCollection(Collection<Object>) in the type WhyNeedWildcard is not applicable for the arguments (List<String>)
    }
}

我們的本意是想建立一個能夠列印所有集合物件中的元素的方法,但是上一篇文章已經說過了,泛型引數不支援繼承,所以編譯不通過。解決這個問題的辦法就是使用萬用字元。

/*
 * 個人主頁:http://hinylover.space
 *
 * Creation Date: 2016年7月2日 下午9:18:04
 */
package demo.blog.java.generic;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 解釋為什麼需要泛型通配。
 * @author xialei
 * @version 1.0 2016年7月2日下午9:18:04
 */
public class WhyNeedWildcard {

    /**
     * 建立一個能夠列印所有集合物件中的元素的方法。
     * @param collection
     */
    public static void printCollection(Collection<?> collection) {
        for (Object object : collection) {
            System.out.println(object);
        }
    }

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("t1");
        list.add("t2");

        printCollection(list); / 編譯通過
    }
}

無邊界

無邊界的萬用字元用

List<?> list = new ArrayList<String>(); // 合法
List<?> list = new ArrayList<?>(); // 不合法
List<String> list = new ArrayList<?>(); // 不合法

對於帶有萬用字元的引用變數,是不能呼叫具有與泛型引數有關的方法的。

List<?> list = new ArrayList<String>();
list.add(1); // 編譯不通過
list.get(0); // 編譯通過
int size = list.size(); // 由於size()方法中不含泛型引數,所以可以在萬用字元變數中呼叫

總結起來,無邊界萬用字元主要用做引用,可以呼叫與泛型引數無關的方法,不能呼叫引數中包含泛型引數的方法。

有邊界

有邊界的萬用字元會對引用的泛型型別進行限定,包括:上邊界通配和下邊界通配。

  1. 上邊界通配,用
List<? extends 型別1> x = new ArrayList<型別2>();

其中,型別2就只能是型別1或者是型別1的子類。下面程式碼驗證合法性。

List<? extends Number> x = new ArrayList<Integer>(); //由於Integer是Number的子類,這是合法的
List<? extends Number> x = new ArrayList<String>();  //由於String不是Number的子類,這是不合法的
  1. 下邊界通配,用
List<? super 型別1> x = new ArrayList<型別2>();

其中,型別2就只能是型別1或者是型別1的超類。下面程式碼有驗證合法性。

List<? super Integer> x = new ArrayList<Number>(); //由於Number是Integer的超類,這是合法的
List<? super Integer> x = new ArrayList<String>();  //由於String不是Integer的超類,這是不合法的

那麼到底什麼時候使用下邊界通配,什麼時候使用上邊界通配呢?首先考慮一下怎樣才能保證不會發生執行時異常,這是泛型要解決的首要問題,通過前面的內容可以看到,任何可能導致型別轉換異常的操作都無法編譯通過。

  • 上邊界通配:可以保證存放的實際物件至多是上邊界指定的型別,那麼在讀取物件時,我們總是可以放心地將物件賦予上邊界型別的引用。
List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);

List<? extends Number> list2 = list1;
Number a = list2.get(0); // 編譯通過
  • 下邊界通配:可以保證存放的實際物件至少是下邊界指定的型別,那麼在存入物件時,我們總是可以放心地將上邊界型別的物件存入泛型物件中。
List<? super Integer> list3 = new ArrayList<Number>();
list3.add(1);
list3.add(2);

總結起來就是:

  • 如果你想從一個數據型別裡獲取資料,使用 ? extends 萬用字元。
  • 如果你想把物件寫入一個數據結構裡,使用 ? super 萬用字元。
  • 如果你既想存,又想取,那就別用萬用字元。

這就是《Effective Java》書中所說的PECS法則(Producer Extends, Consumer Super),Collections工具類中的copy方法就完美地詮釋了這個法則。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
       ...  
}

這個方法的作用是將src列表完整地拷貝到dest列表中。src是原始列表,我們需要讀取其中的元素,所以它是生產者,需要使用extends通配;dest是目標列表,需要將讀取出來的元素存入這個列表中,所以他是消費者,使用super通配。

泛型中的若干問題

泛型與異常

在異常問題上泛型是比較特殊的,主要表現在:

1、泛型類不能繼承Throwable類,所以泛型類的物件既不能捕獲也不能丟擲。下面的程式碼是不合法的。

public class Problem<T> extends Exception {
    ... 
}

假設上邊的程式碼是合法的,那麼我們可以catch住上面的定義的異常類。

try {

} catch (Problem<String> e1) {

} catch (Problem<Integer> e1) {

}

我們知道Problem類編譯後會擦除泛型資訊,相當於同時catch住了兩個相同的異常(都是原始異常類Problem),這在Java中是不予許的。

2、不能在catch子句中使用泛型變數,譬如:

public static <T extends Throwable> void doSomething(Class<T> t){
    try {
        ...
    } catch (T e) { //編譯錯誤
        ...
    }
}

假設上面合法,那麼下面的情況呢?

public static <T extends Throwable> void doSomething(Class<T> t){  
    try {  
        ...  
    } catch(T e) {  
        ...  
    } catch(IndexOutOfBounds e) {  
        ...
    }                           
}

泛型在編譯後會擦除泛型資訊,所有的T都會用Throwable進行替換,如果上面是合法的,那麼下面也是合法的。明顯下面的catch順序是不符合Java語法的。為了避免在執行時產生這種錯誤,所以Java乾脆直接禁止了這種寫法。

public static <T extends Throwable> void doSomething(Class<T> t){  
    try {  
        ...  
    } catch(Throwable e) {  
        ...  
    } catch(IndexOutOfBounds e) {  
        ...
    }                           
}

泛型與基本型別

泛型的實際型別必須是引用型別,而不能是基本型別,所以下面的程式碼都是不合法的。

List<int> list = new ArrayList<int>(); // Syntax error, insert "Dimensions" to complete ReferenceType
List<byte> list = new ArrayList<byte>(); // Syntax error, insert "Dimensions" to complete ReferenceType
...

泛型與陣列

根據《Effective Java》書中所說,泛型與陣列存在兩個重要的不同點:

  1. 陣列是協變的,而泛型不是。如果Son是Father的子型別,那麼Son[]也是Father[]的子型別。而泛型則沒有這種關係。
  2. 陣列是可以具體化的,因此陣列只有在執行時才知道其實際的元素型別。如果企圖將String儲存在Long陣列中,會丟擲ArrayStoreException異常。相比之下,泛型是通過編譯時擦除泛型資訊來實現的,因此,泛型只在編譯時強化型別資訊,並在執行時丟棄型別資訊。

上面的不同點導致陣列與泛型並不能很好的混合使用,下面的程式碼都是不合法的。

new ArrayList<String>[2]; // Cannot create a generic array of ArrayList<String>
new ArrayList<T>[2]; // Cannot create a generic array of ArrayList<T>
new T[2];  // Cannot create a generic array of T

看看下面的程式碼。

List<String>[] array = new ArrayList<String>[];
Object[] objectArray = array;
objectArray[0] = 1;
String c = array[0].get(0);

假設第1行程式碼是合法的,下面的3行程式碼都是沒有問題的。objectArray陣列的0號實際儲存的是一個整型物件,而在第4行程式碼出卻要轉換為String型別,這會發生ClassCastException。異常的引入就是儘量消除這個執行時異常,所以第1行程式碼所示的陣列是不合法的。

泛型例項化

泛型變數是不能被例項化的,這個是筆者在實際開發過程多次遇到過的。

Object c = new T(); // Cannot instantiate the type T

本意是想例項化一個型別為T的物件,但是這樣是無法編譯通過的。可以通過上一篇文章中的示例來實現:

public <T> T getObject(Class<T> t) throws Exception {
    return t.newInstance();
}

泛型與靜態方法和靜態類

泛型類中的靜態變數和靜態方法和不可以使用泛型類所宣告的泛型型別引數,下面的操作是不合法的。

public class Test<T> {  
    public static T one;   //編譯錯誤  

    public static  T show(T one){ //編譯錯誤  
        return null;  
    }  
}  

這是由於泛型的具體引數要在例項化是才能確定,而靜態變數和靜態方法無需例項化就可以呼叫。當物件都還沒有建立時,就呼叫與泛型變數相關的方法,當然是錯誤的。不過,對於泛型方法,靜態泛型方法是可以的,因為具體的泛型型別無需例項化就可以確定。

方法命名衝突

下面來看看由於使用泛型而引起的方法命名衝突問題。

public class Father<T> {

    // 編譯不通過,Name clash: The method equals(T) of type Father<T> has the same erasure as equals(Object) of type Object but does not override it
    public boolean equals(T obj) {
        // TODO Auto-generated method stub
        return super.equals(obj);
    }
}

我們的本意是想寫一個與指定泛型型別物件判斷相等性的方法,這個方法與Objectequals(Object)方法的含義本質上是不同的。Objectequals(Object)是判斷相同型別的物件的相等,而我們定義的方法卻不是這個含義。

以Father為例,看上去過載了Objectequals(Object)方法,因為我們定義的equals方法的引數是String型別的,也就是equals(String)。但是,由於Father在編譯後會擦除泛型資訊,所以equals(T)就被編譯成了equals(Object),這與Objectequals(Object)方法簽名一模一樣,這不就是覆蓋麼!我們並不想覆蓋父類的方法,卻在事實上覆蓋了。如果上面的程式碼可以編譯並執行,對於開發者來說就是一個大坑。唯一的辦法就是改另外一個方法名羅。

其他

泛型規範說明提及另一個原則“要支援擦除的轉換,需要強行制一個類或者型別變數不能同時成為兩個介面的子類,而這兩個子類是同一接品的不同引數化。”
下面的程式碼是非法的:

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

GregorianCalendar會實現Comparable和Compable,是同一個介面的不同引數化實現,這是不允許的。

總結

花了不少時間,將Java泛型中的核心內容整理完了,筆者收穫頗豐,一些在平常開發中不解的問題也得到了解答。型別擦除是Java實現泛型的手段,也是被人說詬病的地方。文中的很多問題都是由型別擦除所引起的,也為開發者挖了不少“坑”。Java的進階之旅還遠沒有結束,接下來還將繼續學習Java。

參考文獻:

相關推薦

系列重新認識Java——特性注意

上一篇文章介紹了Java泛型中的基礎及原理,本文將繼續研究有關Java泛型的內容。本文的主要內容有: 泛型的特性 泛型通配 泛型類與普通類的不同點,也是日常開發要主要的點 泛型特性 泛型的相容性 首先要強調的是,泛型是編譯時才會檢查合法性

系列重新認識Java語言——異常Exception

異常,是Java中非常常用的功能,它可以簡化程式碼,並且增強程式碼的安全性。本文將介紹一些異常高階知識,也是學習Java一來的一次總結。包括以下內內容: 異常的基礎知識 異常特點 異常誤用 如何正確地使用異常 異常的實現原理 關於異常 異常機制,是

Java拓展ArrayListHashMap

  泛型介紹        Java中有泛型這個概念,最開始可能一臉懵逼,因為泛型聽起來很高大上的樣子,其實Java中的泛型就是C++中的模板類(Template),這樣在Java泛型中就可以很輕鬆的建立各種自定義型別的List了,有沒有覺得

C#C# in deep

目的 似的 exc string類型 能夠 出現 pub 檢查 代碼塊 泛型 為什麽要有泛型, 在沒有泛型之前, 什麽東西充當了泛型的作用? 在泛型出現之前, 代碼中會有很多需要強制轉換的地方. 比如 int a = (int) object, 對於這樣類似的代碼, 編譯器

計算機基礎Java學習篇 認識類與物件打包器陣列字串

介紹完基本型別,下面介紹類型別。 瞭解物件與參考的關係,認識打包器,以及陣列物件、字串物件。 一、類與物件       1.建立類          編寫程式需要使用物件(Object),產生物件必須先定義類(Class),類就像是設計圖,而物件是類

一文帶你認識Java基礎

Java泛型基礎 認識泛型 泛型是在JDK1.5之後增加的新功能. 泛型可以解決資料的安全性問題, 主要的原理是在類宣告的時候通過一個標識表示類中某個屬性的型別或者是某個方法的返回值及引數型別. 格式: 訪問許可權 class 類名稱<泛型, …

C#C#關鍵字-where(型別約束)

文章參考:http://blog.csdn.net/startwithdp/article/details/25636737 http://www.cnblogs.com/soundc

JAVA

強制 off 實例 emp 思想 void 成了 意義 依然 一. 泛型概念的提出(為什麽需要泛型)? 首先,我們看下下面這段簡短的代碼: 1 public class GenericTest { 2 3 public static void

Java:入門原理使用

core clas set out keyword getclass code 避免 post 遠在 JDK 1.4 版本的時候,那時候是沒有泛型的概念的。當時 Java 程序員們寫集合類的代碼都是類似於下面這樣: List list = new ArrayList();

什麽是Java正在整理

clas 出現 add 編碼 AI 什麽是 問題 java泛型 list 為什麽要使用泛型? 引入例子 public class GenericTest { public static void main(String[] args) { Lis

java介面方法

泛型介面: 定義一個泛型介面:   通過類去實現這個泛型介面的時候指定泛型T的具體型別。 指定具體型別為Integer: 指定具體型別為String: 指定具體型別為一個自定義的物件: 泛型類: 在編譯器,是無法知道K和V具體是什麼型別,只

Java 引數化型別

Java 泛型 Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。  泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。 一. 泛型概念的提出(為什麼需要泛型)? 首先,我們

Java--上界萬用字元下界萬用字元

轉自:Java泛型中extends和super的區別? 另,問題來源:Java 泛型 <? super T> 中 super 怎麼 理解?與 extends 有何不同?   <? extends T>和<? super T>是Java泛型中的

java的基本介紹使用

 轉載地址  http://m.blog.csdn.net/article/details?id=7864531 現在開始深入學習java的泛型了,以前一直只是在集合中簡單的使用泛型,根本就不明白泛型的原理和作用。泛型在java中,是一個十分重要的特性,所以要好好的研究

Java——你所不知道的那些背後

一、泛型 1、編譯期確定型別安全——泛型(Generics) 泛型是提供給Javac編譯器使用的。可以限定集合中輸入的型別,讓編譯器在編譯期間避免原始程式的非法輸入,編譯器編譯帶型別說明的集合時會去掉“型別”資訊,使程式執行效率不受影響,對

Java 表示集合中儲存的資料的型別

1.儲存字串 //建立一個集合儲存abcd //<E>就代表儲存元素資料的型別 //後面的<> 要跟前面的泛型保持一致 //jdk1.7出來的 菱形泛型 //如果前面聲明瞭泛型,後面的泛型可以省略不寫

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

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

Java 的基本使用介紹

轉載自:http://blog.csdn.net/lonelyroamer/article/details/7864531 現在開始深入學習java的泛型了,以前一直只是在集合中簡單的使用泛型,根本就不明白泛型的原理和作用。泛型在java中,是一個十分重要的特性,所以要

深入理解Java:註解Annotation--註解處理器

display 枚舉 lec con null cto run toolbar int https://www.cnblogs.com/peida/archive/2013/04/26/3038503.html   如果沒有用來讀取註解的方法和工作,那麽註解也就

洛谷P3402 模板可持久化並查集可持久化線段樹,線段樹

std 樹節點 https case 深度 build eof spa 復雜度 orz TPLY 巨佬,題解講的挺好的。 這裏重點梳理一下思路,做一個小小的補充吧。 寫可持久化線段樹,葉子節點維護每個位置的fa,利用每次只更新一個節點的特性,每次插入\(logN\)個節點,