Java基礎知識之泛型詳解

一: 什麼是 java 泛型?
Java 泛型實質就是一種語法約束,泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數,這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。

二:泛型的核心原理是什麼?
泛型的核心原理其實就是泛型的 T 類型參數的原理,Java 編譯器在編譯階段會將泛型代碼轉換爲普通的非泛型代碼,實質就是擦除類型參數 T 替換爲限定類型(上限類型或者下限類型)(無限定類型替換爲Object)且插入必要的強制類型轉換操作,同時 java 編譯器是通過先檢查代碼中泛型的類型然後再進行類型擦除,最後進行編譯的。 編譯器在編譯時擦除所有類型相關信息,所以在運行時不存在任何類型相關信息,例如List在運行時僅用一個List來表示,這樣做的目的是確保能和 Java 5 之前的版本開發二進制類庫進行兼容,我們無法在運行時訪問到類型參數,因爲編譯器已經把泛型類型轉換成了原始類型。

三:泛型的好處是什麼?
泛型的好處其實就是約束,可以讓我們編寫的接口、類、方法等脫離具體類型限定而適用於更加廣泛的類型,從而使代碼達到低耦合、高複用、高可讀、安全可靠的特點,避免主動向下轉型帶來的噁心可讀性和隱晦的轉換異常,儘可能的將類型問題暴露在 IDE 提示和編譯階段

四:泛型類
一般的類只能使用具體的類型,要麼是基本類型,要麼是自定義類型。如果要編寫可以用於多種類型的類呢?那麼我們是不是可以想到用Object來定義類型,的確,用Object完全可以實現存儲任何類型的對象,但是我們在使用容器的時候又想約束容器持有某種類型的對象。因此,與其使用Object,我們更喜歡暫時不指定類型,而是稍後再決定具體使用什麼類型。要達到此目的,需要使用泛型參數,用尖括號括住,放在類名後面,然後在使用這個類的時候,再用實際的類型替換此參數類型即可

public class FanXing<T> {

    public T t;

    public T getT() {
        return t;
    }

    public FanXing(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        FanXing<String> fx1 = new FanXing<>("a");
        FanXing<Integer> fx2 = new FanXing<>(1);
        System.out.println(fx1.getT().getClass());  //java.lang.String
        System.out.println(fx2.getT().getClass());  //java.lang.Integer
    }

}

五:泛型接口

public interface IFanXing<T1, T2> {
    T1 show1(T2 t);

    T2 show2(T1 t);
}
public class FanXing implements IFanXing<String, Integer> {

    @Override
    public String show1(Integer t) {
        return String.valueOf(t);
    }

    @Override
    public Integer show2(String t) {
        return Integer.valueOf(t);
    }

    public static void main(String[] args) {
        FanXing fanXing = new FanXing();
        System.out.println(fanXing.show1(1));  //1
        System.out.println(fanXing.show2("1"));  //1
    }

}

六:泛型方法
到目前爲止,我們看到的泛型,都是應用於整個類上。但同樣可以在類中包含參數化方法,而這個方法所在的類可以是泛型類,也可以不是泛型類。
定義泛型方法只需將泛型參數列表置於返回值之前。
泛型類,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型 。
如果使用泛型方法可以取代泛型類的話,儘量使用泛型方法取代泛型類,因爲它可以使事情更清楚明白

  • 普通泛型方法
public class FanXing {

    public <T> void f(T t){
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {
        FanXing fanXing = new FanXing();
        fanXing.f("");      //java.lang.String
        fanXing.f(1);       //java.lang.Integer
        fanXing.f(1.0);     //java.lang.Double
        fanXing.f(1.0F);    //java.lang.Float
        fanXing.f('a');     //java.lang.Character
    }

}
  • 靜態泛型方法

靜態方法無法訪問泛型類的類型參數,如果方法需要使用泛型能力,就必須使其成爲泛型方法
如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法 。

public class FanXing<T>{

    private T t;

    public static <T> void e1(){
        //無法訪問泛型類的類型參數t
    }

    //編譯器報錯
//    public static void e(T t){
//        
//    }

    /**
     * 如果在類中定義使用泛型的靜態方法,需要添加額外的泛型聲明(將這個方法定義成泛型方法)
     * 即使靜態方法要使用泛型類中已經聲明過的泛型也不可以。
     * 如:public static void e(T t){..},此時編譯器會提示錯誤信息:
     "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void f(T t){
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {
        FanXing.f("");      //java.lang.String
        FanXing.f(1);       //java.lang.Integer
        FanXing.f(1.0);     //java.lang.Double
        FanXing.f(1.0F);    //java.lang.Float
        FanXing.f('a');     //java.lang.Character
    }

}
    • 可變參數泛型方法
      泛型方法與可變參數能夠很好地共存
public class FanXing {

    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<>();
        for (T item : args) {
            result.add(item);
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(FanXing.makeList("a","b","c","d"));//[a, b, c, d]
    }

}

七:泛型數組

關於泛型數組,官方文檔上面說是:在java中是不能創建一個確切的泛型類型的數組的
一般的解決方案是:在任何想要創建泛型數組的地方都使用ArrayList摘自《Java編程思想》

也就是說下面我們這樣創建泛型數組是不允許的:(右邊由於編譯後擦除數據類型,導致變爲Object數據類型,此時可以插入各種數據類型,當賦值給左邊的時候還會進行類型強轉爲String的時候會報錯,詳細可以看下面的例子

public class FanXing<T> {}
FanXing<String>[] fx1 = new FanXing<String>[10];                //error
FanXing<String>[] fx2 = (Fanxing<String>[])new Object[10];      //error

其問題在於數組將跟蹤他們的實際類型,而這個類型是在數組被創建的時候確定的。即使fx2已經被轉型爲Fanxing< String>[]類型,但是這個信息只存在編譯期,運行期它仍然是Object數組。

然而真的沒辦法創建數組了嗎?答案當然是有的

成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型摘自《Java編程思想》
我們看下面這個創建,編譯器是通過的

public class FanXing2 {
    public static void main(String[] args) {
        FanXing<String>[] fxArray = (FanXing<String>[]) new FanXing[10];    //OK
        fxArray[0] = new FanXing<String>();   //OK
  //      fxArray[1] = new Object();            //error
  //      fxArray[2] = new FanXing<Integer>();  //error 
    }
}

下面我們再引用sun官方文檔的例子:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

這種情況下,由於JVM泛型的擦除機制,在運行時JVM是不知道泛型信息的,所以可以給oa[1]賦上一個ArrayList而不會出現異常,但是在取出數據的時候卻要做一次類型轉換,所以就會出現ClassCastException,如果可以進行泛型數組的聲明,上面說的這種情況在編譯期將不會出現任何的警告和錯誤,只有在運行時纔會出錯。
而對泛型數組的聲明進行限制,對於這樣的情況,可以在編譯期提示代碼有類型安全問題,比沒有任何提示要強很多。

下面採用通配符的方式是被允許的:數組的類型不可以是類型變量,除非是採用通配符的方式,因爲對於通配符的方式,最後取出數據是要做顯式的類型轉換的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK 

八:泛型的擦除
1、什麼是泛型的擦除?

泛型的擦除是指:java編譯器在編譯階段將泛型的代碼轉變成非泛型代碼的過程。
泛型類型只有在靜態類型檢查期間纔出現,在此以後程序中的所有泛型類型都將被擦除,替換爲他們的非泛型上界。如List這樣的類型將被擦除爲List,而普通的類型變量在未指定邊界的情況下將被擦除爲Object

2、泛型擦除的原因

因爲泛型不是java出現時就有的組成部分,泛型是Java SE5纔出現的,那麼爲了兼容JavaSE5之前的非泛型類庫,所以java才採用了折中的辦法,即擦除。

3、泛型擦除的代價

泛型不能用於顯式地引用運行時類型的操作之中,例如轉型、instanceof操作和new表達式。因爲所有關於參數的類型信息都丟失了。

下面看一個泛型擦除的例子:

public class FanXing {

    public static void main(String[] args) {
        List<String> stringList=new ArrayList<>();
        List<Integer> integerList=new ArrayList<>();
        Class stringClass=stringList.getClass();
        Class integerClass=integerList.getClass();
        System.out.print(stringClass == integerClass);     //true
    }

}
在代碼的第4行和第5行,我們分別定義了一個接受String類型的List和一個接受Integer類型的List,按照我們正常的理解,泛型ArrayList雖然是相同的,
但是我們給它傳了不同的類型參數,那麼c1和2的類型應該是不同的。但是結果恰恰想法,運行程序發現二者的類型時相同的。然後我們來反編譯這個類來看一下它的字節碼:

 

 

 

由上圖我們可以看出無論是ArrayList< String>還是ArrayList< Integer>都被擦除爲List,最終都是Object去getClass,可見沒有任何一點類型信息的事。
因此:在泛型代碼的內部,無法獲得任何有關泛型參數類型的信息。

接下來我們再看下一個例子:

public class FanXing<T> {

    public void test(Object arg){
        if (arg instanceof T){}     //error
        T a=new T();                //error
        T[] array=new T[100];       //error
    }

}

當我們嘗試去創建T的對象以及instanceof等操作時,都是不能編譯通過的,因爲編譯器在編譯的時候已經將類型T給擦除了。
任何在運行時需要知道確切類型信息的操作都將無法工作。

九:通配符

泛型通配符也是一個難點,不過只要理解了,之後寫代碼就會使你的代碼更加健壯。
通配符可以分爲上界、下界、無界通配符。
爲了方便理解,我們需要寫幾個輔助類,Cat和Bird都繼承於Animal,Magpie繼承於Bird

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.print(getName() + ":eat");
    }

    public String getName() {
        return name;
    }
}
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    public void jump(){
        System.out.print(getName()+" jump");
    }
}
public class Bird extends Animal {

    public Bird(String name) {
        super(name);
    }

    public void fly() {
        System.out.print(getName() + " fly");
    }
}
public class Magpie extends Bird {

    public Magpie(String name) {
        super(name);
    }

    public void sing() {
        System.out.print(getName() + " sing");
    }
}

首先我們先來看一下不使用通配符的情況。

public class TestAnimal {
    public void traverse(List<Animal> list) {
        for (Animal animal : list) {
            animal.eat();
        }
    }

    public static void main(String[] args) {

        TestAnimal testAnimal = new TestAnimal();

        List<Animal> animalList = new ArrayList<>();
        animalList.add(new Cat("cat"));
        animalList.add(new Bird("bird"));
        testAnimal.traverse(animalList);    //編譯通過

        List<Bird> birdList = new ArrayList<>();
        birdList.add(new Bird("bird"));
        birdList.add(new Magpie("magpie"));
        testAnimal.traverse(birdList);      //編譯未通過

    }
}

上面例子對於animalList 編譯通過應該都能理解,因爲Cat和Bird都屬於Animal,所以能添加到animalList裏面。對於下面編譯未通過是因爲List< Bird>並不是List< Animal>的子類,出入參數有誤,因此無法通過編譯。

下面我們嘗試去修改traverse的代碼,使他變得通用一些,不僅能接受List< Animal>參數還能接受List< Bird>參數。

通配符的上界
既然知道List< Bird>並不是List< Anilmal>的子類型,那就需要去尋找替他解決的辦法, 使traverse方法變得更爲通用(既可以接受List< Animal>類型,也可以接受List< Bird>等參數)。在java裏解決辦法就是使用通配符“?”,具體到traverse,就是將方法改爲traverse(List<? extends Animal> list),當中“?”就是通配符,而“? extends Animal”則表示通配符“?”的上界爲Animal,換句話說就是,“? extends Animal”可以代表Animal或其子類,可代表不了Animal的父類(如Object),因爲通配符的上界是Animal。如下,爲改進之後的traverse

public class TestAnimal {
    public void traverse(List<? extends Animal> list) {
        for (Animal animal : list) {
            animal.eat();
        }
    }

    public static void main(String[] args) {

        TestAnimal testAnimal = new TestAnimal();

        List<Animal> animalList = new ArrayList<>();
        animalList.add(new Cat("cat"));
        animalList.add(new Bird("bird"));
        testAnimal.traverse(animalList);    //編譯通過

        List<Bird> birdList = new ArrayList<>();
        birdList.add(new Bird("bird"));
        birdList.add(new Magpie("magpie"));
        testAnimal.traverse(birdList);      //編譯通過

    }
}

經過上述分析,可以知道List< Animal>和List< Cat>都是List< ? extends Animal>的子類型,類似有List< Bird>,List< Magpie>也是List< ? extends Animal>的子類型。現總結如下,對於通配符的上界,有以下幾條基本規則:(假設給定的泛型類型爲G,(如List< E>中的List),兩個具體的泛型參數X、Y,當中Y是X的子類(如上的Animal和Cat))

  1. G< ? extends Y> 是 G< ? extends X>的子類型(如List< ? extends Cat> 是 List< ? extends Animal>的子類型)。
  2. G< X> 是 G< ? extends X>的子類型(如List< Animal> 是 List< ? extends Animal>的子類型)
  3. G< ?> 與 G< ? extends Object>等同,如List< ?> 與List< ? extends Objext>等同。

下面我們再來看一段代碼:

public class TestAnimal {
    public void testAdd(List<? extends Animal> list){
        list.add(new Animal("animal"));         //編譯不通過
        list.add(new Bird("bird"));             //編譯不通過
        list.add(new Cat("cat"));               //編譯不通過
    }

    public static void main(String[] args) {
        List<? extends Animal> animalList = new ArrayList<>();
        animalList.add(new Cat("cat"));         //編譯不通過
        animalList.add(new Bird("bird"));       //編譯不通過
    }
}

無法通過編譯?是的,真的是無法通過編譯!!!那到底是爲什麼呢?
在解釋之前,再來重新強調一下已經知道的規則:在List< Aimal> list裏只能添加Animal類對象及其子類對象(如Cat和Bird對象),在List< Bird>裏只能添加Bird類和其子類對象(如Magpie),可不能添加Animal對象(不是Bird的子類),類似的在List< Cat>和List< Magpie>裏只能添加Cat和Bird對象(或其子類對象,不過這沒有列出)。現在再回頭看一下testAdd()方法,我們知道List< Animal>、List< Cat>等都是List< ? extends Animal>的子類型。先假設傳入的參數爲爲List< Animal>,則第一段代碼的三個“add”操作都是可行的;可如果是List< Bird>呢??則只有第二個“add”可以執行;再假設傳入的是List< Tiger>(Tiger是想象出來的,可認爲是Cat的子類),則三個“add”操作都不能執行。

現在反過來說,給testAdd傳入不同的參數,三個“add”操作都可能引發類型不兼容問題,而傳入的參數是未知的,所以java爲了保護其類型一致,禁止向List< ? extends Animal>添加任意對象,不過卻可以添加 null,即list.add(null)是可行的。有了上面談到的基礎,再來理解第二段代碼就不難了,因爲List< ? extends Animal>的類型“? extends Animal”無法確定,可以是Animal,Bird或者Cat等,所以爲了保護其類型的一致性,也是不能往list添加任意對象的,不過卻可以添加 null。

總結如下:不能往List< ? extends Animal> 添加任意對象,除了null。但是子類對象可以作爲方法參數賦值給右邊

  • 通配符的下屆:

既然有了通配符的上界,自然有着通配符的下界。可以如此定義通配符的下界 List< ? super Bird>,其中”Bird“就是通配符的下界。注意:不能同時聲明泛型通配符申明上界和下界。在談注意細節之前,我們先看一下通配符的使用規則——對於通配符的上界,有以下幾條基本規則:(假設給定的泛型類型爲G,(如List< E>中的List),兩個具體的泛型參數X、Y,當中Y是X的子類(如上的Animal和Cat))G< ? super X> 是 G< ? super Y>的子類型(如List< ? super Animal> 是 List< ? super Bird>的子類型)。G< X> 是 G< ? super X>的子類型(如List< Animal> 是 List< ? super Animal>的子類型)

我們再來看下面的代碼

public class TestAnimal {
    public void testAdd(List<? super Bird> list){
        list.add(new Bird("bird"));             //編譯通過
        list.add(new Magpie("magpie"));         //編譯通過
    }

    public static void main(String[] args) {
        List<? super Bird> animalList = new ArrayList<>();
        animalList.add(new Bird("bird"));       //編譯通過
        animalList.add(new Magpie("magpie"));   //編譯通過
        animalList.add(new Animal("animal"));   //編譯不通過
    }
}

testAdd 方法 沒錯,編譯通過!!!
在解疑之前,再來強調一個知識點,子類可以指向父類,即Bird也是Animal對象。現在考慮傳入到testAdd()的所有可能的參數,可以是List< Bird>,List< Animal>,或者List< Objext>等等,發現這些參數的類型是Bird或其父類,那我們可以這樣看,把bird、magpie看成Bird對象,也可以將bird、magpie看成Animal對象,類似的可看成Object對象,最後發現這些添加到List< ? supe Bird> list裏的對象都是同一類對象,因此testAdd方法是符合邏輯,可以通過編譯的。

現在再來看一下第二段代碼對於,第二、三行代碼的解釋和上文一樣,至於最後一行“list.add(new Animal(“animal”))”是無法通過編譯的,爲什麼的??爲了保護類型的一致性,因爲“? super Bird”可以是Animal,也可以是Object或其他Bird的父類,因無法確定其類型,也就不能往List< ? super Bird>添加Bird的任意父類對象。可以添加比下界小的值。

  • 無界通配符

知道了通配符的上界和下界,其實也等同於知道了無界通配符,不加任何修飾即可,單獨一個“?”。如List< ?>,“?”可以代表任意類型,“任意”也就是未知類型。
List< Object>表示:持有任何Object類型的原生List,只准對Object類型,注意List<Object>不是List<其他類型>的父類
List< ?>表示:具有某種特定類型的非原生List,可以爲有所類型。
當方法作爲List< Object>作爲類型參數時:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + "");
    System.out.println();
}

可以選擇改爲如下實現

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + "");
    System.out.println();
}

這樣就可以兼容更多的輸出,而不單純是List< Object>,如下:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

對於List<T>和List<?>的區別

通俗地說,"T"是定義類或方法時聲明的東西,"?"是調用時傳入的東西,二者是不同的概念。

當在外面使用一個帶泛型T的類或方法時,T應該用一個實際的數據類型替換它,這裏的數據類型可以是Integer,String,Boolean,也可以是"?"(wildcard,通配符)。

有兩種情況可以傳入"?":

1、使用過程中僅用到Object的方法,跟T的具體類型無關,像equals()等等,因爲任何一個泛型肯定是Object的子類;

2、使用過程中不依賴於泛型。最典型的是Class<?>,因爲Class類的方法大多跟泛型無關

是java泛型的兩種用法:List<T>是泛型方法,List<?>是限制通配符

List<T>一般有兩種用途:
1、定義一個通用的泛型方法。
僞代碼:

public interface Dao{
List<T> getList(){};
}

List<String> getStringList(){
return dao.getList();//dao是一個實現類實例
}

List<Integer> getIntList(){
return dao.getList();
}

上面接口的getList方法如果定義成List<?> ,後面就會報錯。‘

2、限制方法的參數之間或參數和返回結果之間的關係。
List<T> getList<T param1,T param2>
這樣可以限制返回結果的類型以及兩個參數的類型一致。

List<?>一般就是在泛型起一個限制作用。
僞代碼:

public Class Fruit(){}

public Class Apple extends Fruit(){}

public void test(? extends Fruit){};

test(new Fruit());
test(new Apple());
test(new String()); //這個就會報錯

總結:T就是在定義類或者方法時聲明的東西,?是調用時傳入的東西,傳入各種類型都可以。

十:泛型實戰

1、請嘗試解釋下面程序中每行代碼執行情況及原因?

public class Test{
    public static <T> T add(T x, T y){  
        return y;
    }

    public static void main(String[] args) {
        int t0 = Test.add(10, 20.8);
        int t1 = Test.add(10, 20);
        Number t2 = Test.add(100, 22.2);
        Object t3 = Test.add(121, "abc");

        int t4 = Test.<Integer>add(10, 20);
        int t5 = Test.<Integer>add(100, 22.2);
        Number t6 = Test.<Number>add(121, 22.2);
    }
}  

t0 編譯直接報錯,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級爲 Number,故 T 爲 Number 類型,而 t0 類型爲 int,所以類型錯誤。
t1 執行賦值成功,add 的兩個參數都是 Integer,所以 T 爲 Integer 類型。
t2 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級爲 Number,故 T 爲 Number 類型。
t3 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,所以取同一父類的最小級爲 Object,故 T 爲 Object 類型。
t4 執行賦值成功,add 指定了泛型類型爲 Integer,所以只能 add 爲 Integer 類型或者其子類的參數。
t5 編譯直接報錯,add 指定了泛型類型爲 Integer,所以只能 add 爲 Integer 類型或者其子類的參數,不能爲 Float。
t6 執行賦值成功,add 指定了泛型類型爲 Number,所以只能 add 爲 Number 類型或者其子類的參數,Integer 和 Float 均爲其子類,所以可以 add 成功。
2、請嘗試解釋下面程序編譯到運行的現象及原因?

ArrayList<String> arrayList1 = new ArrayList();     // 編譯警告
arrayList1.add("1");    // 編譯通過  
arrayList1.add(1);      // 編譯錯誤
String str1 = arrayList1.get(0);        //返回類型就是 String  

ArrayList arrayList2 = new ArrayList<String>();     // 編譯警告
arrayList2.add("1");    // 編譯通過  
arrayList2.add(1);      // 編譯通過  
Object object = arrayList2.get(0);  // 返回類型就是 Object  

List<String> arrayList3 = new ArrayList<>();    //JDK7的新特性,會自動推斷泛型
arrayList3.add("123");  //編譯通過
arrayList3.add(123);    //編譯錯誤

new ArrayList<String>().add("11");  // 編譯通過
new ArrayList<String>().add(22);    // 編譯錯誤  
String string = new ArrayList<String>().get(0); // 返回類型就是 String  

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

arrayList1 其實可以實現與完全使用泛型參數一樣的效果,arrayList2 完全沒了泛型特性,arrayList3 是 JDK 7 支持的中規中矩寫法,由於類型檢查就是針對引用的,與引用的對象無關,所以有了這些結果。

arrayList4 與 arrayList5 是因爲泛型中參數化類型是不支持繼承關係的,切記(注意與 arrayList1 和 arrayList2 對比理解)。至於不支持的原因如下:

對於 arrayList4 首先我們假設 new ArrayList< Object>() 申請的內存賦值給了引用類型是ArrayList< Object>或 ArrayList 的 temp,接着我們向 temp 中添加了幾個元素(支持 add 任何類型), 然後讓ArrayList< String> arrayList4 = temp,這時候調用 arrayList4 的 get 返回的都是 String 類型(類型檢測是根據引用來決定的),鍋來了,temp 中存了各種奇怪的類型啊, arrayList4 拿回來直接用就可能出現 ClassCastException 異常啊,臥槽,Java 設計泛型的目的之一就是爲了避免出現這種鍋啊,所以 Java 直接不允許進行這樣的引用傳遞,所以直接編譯報錯,扼殺在搖籃。

同理對於 arrayList5 來說類似 arrayList4,只不過最終 get 出來是從 String 轉爲 Object,不會出現 ClassCastException 異常,但是一樣臥槽啊,我還得自己轉,毛線,泛型出現之一也是爲了解決這個鍋啊,所以 Java 直接不允許進行這樣的引用傳遞,所以直接編譯報錯,扼殺在搖籃。(ArrayList<Object>不是ArrayList<Object>的父類,不能賦值,兩邊要求類型一致纔可以,否則要用通配符)

3、請嘗試解釋下面程序編譯到運行的現象及原因?

List<String>[] ls1 = new ArrayList<String>[10]; //1
List<String>[] ls2 = new ArrayList[10]; //2
List<?>[] lsa = new List<?>[10];    //3

運行結果
1 編譯錯誤。

2 正常運行,但是編譯有警告。

3 正常運行。

因爲 Java 規定數組的類型不可以是泛型類型變量,除非是採用通配符的方式。數組是允許把一個子類數組賦給一個父類數組變量的,譬如 Father 繼承自 Son,則可以定義Father[] son=new Son[10];,如果 Java 允許泛型數組這會出現如下代碼:

List<String>[] ls1 = new ArrayList<String>[10];
Object[] oj = ls1;

也就是我們能在數組 ls1 裏存放任何類的對象且能夠通過編譯,因爲在編譯階段 ls1 被認爲是一個Object[],也就是 ls1 裏面可以放一個 int、也可以放一個 String,當我們運行階段取出裏面的 int 並強制轉換爲 String 則會出現 ClassCastException,這明顯違反了泛型引入的原則,所以 Java 不允許創建泛型數組。對於 2 可以編譯通過但是會得到警告的解釋其實是因爲編譯器確實不讓我們實例化泛型數組,但是允許我們創建對這種數組的引用,所以我們可以創建非泛型數組轉型給泛型引用。對於 3 通配符的方式,最後取出數據是要做顯式的類型轉換的,所以並不會存在上一個例子的問題。
4、請嘗試解釋下面程序編譯到運行的現象及原因?

public class Test1<T> {    
    public static T value;   //1
    public static  T test1(T param){ //2
        return null;    
    }    
}

public class Test2<T> {    
    public static <T> T test2(T param){ //3
        return null;
    }    
}

class Test<T> {  //4 這個類可以運行嗎?
    public boolean equals(T value) {  
        return true;  
    }     
}

解答:

1 編譯錯誤,2 編譯錯誤,3 正常運行。

 

 

因爲泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數,泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用,對象都沒創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的;而 3 之所以成功是因爲在泛型方法中使用的 T 是自己在方法中定義的 T,而不是泛型類中的 T。

4 無法編譯通過,因爲擦除後方法 boolean equals(T) 變成了方法 boolean equals(Object),這與 Object.equals 方法是衝突的,除非重新命名引發錯誤的方法。

5、下面程序編譯運行會有什麼現象?

Vector<? extends Number> x1 = new Vector<Integer>();    //正確
Vector<? extends Number> x2 = new Vector<String>(); //編譯錯誤

Vector<? super Integer> y1 = new Vector<Number>();  //正確
Vector<? super Integer> y2 = new Vector<Byte>();    //編譯錯誤

解釋:

本題主要考察泛型中的?通配符的上下邊界擴展問題。

通配符對於上邊界有如下限制:Vector< ? extends 類型1> x = new Vector<類型2>();中的類型1指定一個數據類型,則類型2就只能是類型1或者是類型1的子類。

通配符對於下邊界有如下限制:Vector< ? super 類型1> x = new Vector<類型2>();中的類型1指定一個數據類型,則類型2就只能是類型1或者是類型1的父類。

6、簡單說說 Java 中List< Object>和原始類型List之間的區別?

解答:

區別一:原始類型和帶泛型參數類型< Object>之間的主要區別是在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查,通過使用 Object 作爲類型可以告知編譯器該方法可以接受任何類型的對象(比如 String 或 Integer)。

區別二:我們可以把任何帶參數的類型傳遞給原始類型 List,但卻不能把List傳遞給接受List的方法,因爲會產生編譯錯誤。

7、Java 中List< ?>和List< Object>之間的區別是什麼?

解答:

這道題跟上一道題看起來很像,實質上卻完全不同。List< ?>是一個未知類型的List,而List< Object>其實是任意類型的List。我們可以把List< String>, List< Integer>賦值給List< ?>,卻不能把List < String>賦值給List< Object>。譬如:

List<?> listOfAnyType;
List<Object> listOfObject = new ArrayList<Object>();
List<String> listOfString = new ArrayList<String>();
List<Integer> listOfInteger = new ArrayList<Integer>();
listOfAnyType = listOfString; //legal
listOfAnyType = listOfInteger; //legal
listOfObjectType = (List<Object>) listOfString; //compiler error – in-convertible types

所以通配符形式都可以用類型參數的形式來替代,通配符能做的用類型參數都能做。 通配符形式可以減少類型參數,形式上往往更爲簡單,可讀性也更好,所以能用通配符的就用通配符。 如果類型參數之間有依賴關係或者返回值依賴類型參數或者需要寫操作則只能用類型參數。

8、List< ? extends T>和List < ? super T>之間有什麼區別?

解答:

有時面試官會用這個問題來評估你對泛型的理解,而不是直接問你什麼是限定通配符和非限定通配符,這兩個List的聲明都是限定通配符的例子,List

public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)
public static <T> void copy(List<? super T> dest, List<? extends T> src)
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)


9、< T extends E>和< ? extends E>有什麼關係?

解答:

它們用的地方不一樣,< T extends E>用於定義類型參數,聲明瞭一個類型參數 T,可放在泛型類定義中類名後面、泛型方法返回值前面。 < ? extends E>用於實例化類型參數,用於實例化泛型變量中的類型參數,只是這個具體類型是未知的,只知道它是 E 或 E 的某個子類型。 雖然它們不一樣,但兩種寫法經常可以達到相同的目的,譬如:

public void addAll(Bean<? extends E> c)
public <T extends E> void addAll(Bean<T> c)

 



 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章