面試重災區-泛型攻克

目錄介紹

  • 1.使用泛型的意義
  • 1.1 例一
  • 1.2 例二
  • 2.泛型擦除
  • 3.使用泛型帶來的問題
  • 4.泛型的通配符super和extends

泛型能夠說是面試中的重災區了,一直以來你們對於泛型的認識可能並非很是的清晰,在泛型的使用上可能就更疑惑了,這篇文章將帶你們攻克這一知識點,泛型這塊其實仍是要你們多敲敲,看看什麼狀況下泛型是會報錯的什麼狀況下不會,這樣才能真正的瞭解泛型。

1. 使用泛型的意義

1.泛型的創造者讓泛型的使用者能夠在使用泛型的時候根據傳入泛型類型的不一樣而使用對應類型的API。java

2.使用泛型能夠解決沒必要要的類型轉換錯誤。android

針對第一點:舉系統用到泛型的兩個例子:程序員

例子一:findViewById()面試

@Nullable
public <T extends View> T findViewById(@IdRes int id) {
    return getWindow().findViewById(id);
}
複製代碼

這個方法應該是咱們平常開發中最經常使用的方法了,能夠看到根據咱們傳入的id最終解析找到返回與之對應的一個View。 層層深刻最終找到源頭:數組

protected <T extends View> T findViewTraversal(@IdRes int id) {
    if (id == mID) {
        return (T) this;
    }
    return null;
}
複製代碼

調用時:bash

TextView textView= findViewById(R.id.title);
textView.setText("123");

ImageView imageView= findViewById(R.id.image);
imageView.setImageResource(R.drawable.ic_smoll_android);
複製代碼

能夠看到咱們並無顯示的獲取或者建立對應的TextView或者ImageView實例就直接獲取到了對應的類型從而調用到了對應類型的API,好比TextView的setText方法和ImageView的setImageResource方法。app

這裏的泛型創造者也就是谷歌寫View的這個程序員,泛型的使用者天然就是咱們自身調用findViewById這個方法的人了,因爲這是一個泛型方法,咱們最終不用new TextView就能夠獲取對應的TextView實例,由於泛型的創造者已經幫咱們將新建的邏輯寫好了,咱們只須要根據須要用不用的View類型去接收就能夠獲取到對應類型的實例,這無疑是至關方便的。 這就是根據咱們傳入類型不一樣而獲取調用到對應類型API的一個最好的例子。性能

例子二:系統的Comparable接口ui

public interface Comparable<T> {
    public int compareTo(T o);
}
複製代碼
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
...
    public native int compareTo(String anotherString);

}
複製代碼
public final class Integer extends Number implements Comparable<Integer> {
...
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }

}
複製代碼

能夠看到String和Integer類實現Comparable接口後經過傳入不一樣的泛型類型String,Integer從而在複寫compareTo方法時即可以調用到傳入的類型String,Integer的API。this

第一點的總結:

  1. 咱們使用泛型時判斷是否須要使用泛型最簡單的方法其實就是判斷當前這個類中的方法是否須要在返回值中使用泛型,若是須要則可使用泛型,反則不須要。
  2. 能夠看到例子二實際上是違背了我上邊所說的第一點的,可是若是你仔細看就會發現這個用法其實最終也是基於整體原則: 使用泛型的時候根據傳入的不一樣類型而使用對應類型的API 而去使用的,因此也是知足這個原則的。
  3. 若是你的類型肯定的話是徹底不須要使用泛型的。

針對第二點:

List list = new ArrayList();
list.add("123");
int a = (int) list.get(0);//須要作類型強轉,在運行時,ClassCastException
複製代碼

能夠看到若是沒有泛型在運行時有可能會出現這樣沒必要要的類型轉換錯誤,使用泛型之後在編譯時就將類型約束好了從而解決了這種問題的產生。

2.泛型擦除

Java中的泛型類型在代碼運行時會被擦除掉(通常狀況下會被擦成Object類型,若是使用了上限通配符的話會被擦成extends右邊的類型,如T extends View則最終會被擦成View類型),也就是說泛型只在編譯期起做用。

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2); true
複製代碼

能夠看到最後結果是true的,由於泛型類型在運行時都會被擦除掉,也就是說其實c1和c2是由相同的class字節碼文件加載出來的,他們是相同的Class,new ArrayList()和new ArrayList()最後都會被擦成new ArrayList()。

爲何要將泛型類型在運行時擦除?

最主要仍是出於兼容性的考慮,泛型是JDK1.5之後才引入的,爲了兼容以前的JDK版本因此在運行時將泛型類型都擦掉以保證和以前JDK版本的java字節碼相同。

public class Test<T> {
    private T b;
    public void setB(T b) {
        this.b = b;
    }
    public T getB() {
        return b;
    }
}


Test<Integer> a=new Test<Integer>();
a.setB(1);
int b=a.getB();//不須要作類型強轉,自動完成
複製代碼
//定義處已經被擦除成Object,沒法進行強轉,不知道強轉成什麼public T getB();
   Code:
      0: aload_0
      1: getfield      #23 // Field b:Ljava/lang/Object;
      4: areturn
//調用處利用checkcast進行強轉
L5 {
            aload1
            invokevirtual com/ljj/A getB()Ljava.lang.Object);
            checkcast java/lang/Integer
            invokevirtual java/lang/Integer intValue(()I);
            istore2
    }
複製代碼

能夠看到若是使用了泛型,在運行期間是會自動進行類型強轉的而不用咱們主動去調用類型強轉。

Java 泛型類、泛型接口、泛型方法有什麼區別?

  1. 泛型類是在實例化類的對象時才能肯定的類型,其定義譬如 class Test {},在實例化該類時必須指明泛型 T 的具體類型。

  2. 泛型接口與泛型類同樣,其定義譬如 interface Generator { E dunc(E e); }。

  3. 泛型方法是獨立的,它能夠不依附與你當前類中的泛型,當前類沒有泛型也可使用泛型方法使用的時候將泛型類型定義到方法返回值的左邊,以後就可使用了。

<T> void func(T val) {}
<T> T func(Fruit val) {}
public static <T> void func(T val) {}
複製代碼

下邊舉一個例子

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);

    }
}
複製代碼
  1. t0 編譯直接報錯,add 的兩個參數一個是 Integer,一個是 Float,因此取同一父類的最小級爲 Number,故 T 爲 Number 類型,而 t0 類型爲 int,因此類型錯誤。
  2. t1 執行賦值成功,add 的兩個參數都是 Integer,因此 T 爲 Integer 類型。
  3. t2 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,因此取同一父類的最小級爲 Number,故 T 爲 Number 類型。
  4. t3 執行賦值成功,add 的兩個參數一個是 Integer,一個是 Float,因此取同一父類的最小級爲 Object,故 T 爲 Object 類型。
  5. t4 執行賦值成功,add 指定了泛型類型爲 Integer,因此只能 add 爲 Integer 類型或者其子類的參數。
  6. t5 編譯直接報錯,add 指定了泛型類型爲 Integer,因此只能 add 爲 Integer 類型或者其子類的參數,不能爲 Float。
  7. t6 執行賦值成功,add 指定了泛型類型爲 Number,因此只能 add 爲 Number 類型或者其子類的參數,Integer 和 Float 均爲其子類,因此能夠 add 成功。

數組不支持泛型

Fruit<String>[] i=new Fruit<String>[10]; //Errot
Fruit<?>[] i=new Fruit<?>[10]; /能夠經過,可是沒有意義
複製代碼

再看一個例子

//Part1
List<Object> list=new ArrayList<String>();//Error
list.add("123");

//Part2
Object[] objects=new Long[10];
objects[0]="123"; //Runtime 異常
複製代碼

上面 Part 1 編譯出錯,Part 2 編譯 OK,運行出錯。 由於 List 和 ArrayList 沒有繼承關係,而 Java 的數組是在運行時類型檢查的。

3.使用泛型帶來的問題

1.自動拆箱裝箱帶來的性能損耗

泛型不支持傳入基本數據類型,只支持引用數據類型,例如咱們直接使用new ArrayList()是不合法的,由於類型擦除後會替換成Object(若是經過extends設置了上限,則替換成上限類型),int顯然沒法替換成Object,因此泛型參數必須是引用類型。

2.泛型類型沒法當作真實類型去使用

因此下列方法都是錯誤的

static <T> void test(T t){ //所有ERROR
    T newInstance=new T();  
    T[] array=new T[0]; 
    Class c=T.class;
    List<T> list=new ArrayList<T>();
    if(list instanceof List<String>){}  
}
複製代碼

3.泛型會自動將類型進行強轉,類型轉換時也會有性能的開銷。

如何經過反射獲取泛型類型?

既然泛型類型在運行時會被擦除那麼咱們怎麼獲取到泛型類型呢?

其實在泛型擦除時並不會將全部的泛型類型都擦除掉,它只會擦除運行時的泛型類型,編譯時類中定義的泛型類型是不會被擦除的,對應的泛型類型會被保存在Signature中。 咱們若是想獲取對應對象中的泛型類型只需將動態建立的對象改成匿名內部類便可獲取,由於內部類實在編譯時建立的,泛型類型是會保存下來的。 對應API getGeneric...都是獲取泛型類型的。 下邊舉兩個例子:

List<Integer> list = new ArrayList<>();
list.getClass().getGenericSuperclass(); //獲取不到泛型信息
List<Integer> list1 = new ArrayList() {};
list1.getClass().getGenericSuperclass(); //能夠獲取到泛型信息
複製代碼
  1. 能夠看到第一個list因爲是在運行時建立的對象因此因爲泛型擦除是沒法獲取泛型信息的,由於運行時對象本質是方法的調用(真正調用了之後纔會建立),在運行時建立的對象是沒有辦法經過反射獲取其中的類型的。
  2. 第二個是能夠獲取的,由於後邊加了{},這就使得這個list成爲了一個匿名內部類且父類是List,子類是能夠調用父類的構造方法的,加了以後這個list1就不是運行時建立的對象了而是編譯時建立的,因此是能夠獲取泛型類型的。

下邊以一道題爲例:

public class Demo {

        public static void main(String[] args) throws Exception {

                ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
                System.out.println(type.getActualTypeArguments()[0]);

                ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
                System.out.println(fieldType.getActualTypeArguments()[0]);

                ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0];
                System.out.println(paramType.getActualTypeArguments()[0])
    
                System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]);
        }

        class Foo<T extends CharSequence> {

                public List<Bar> children = new ArrayList<Bar>();

                public List<StringBuilder> foo(List<String> foo) {return null}

                public void bar(List<? extends String> param) {}
         }

        class Bar extends Foo<String> {}
}
複製代碼

運行結果以下。

class java.lang.String

class Demo$Bar

class java.lang.String

interface java.lang.CharSequence

經過上面例子會發現泛型類型的每個類型參數都被保留了,並且在運行期能夠經過反射機制獲取到,由於泛型的擦除機制實際上擦除的是除結構化信息外的全部東西(結構化信息指與類結構相關的信息,而不是與程序執行流程有關的,即與類及其字段和方法的類型參數相關的元數據都會被保留下來經過反射獲取到)。

4.泛型的通配符super和extends

因爲泛型不是協變的,它不支持繼承,好比在使用 List< Number> 的地方不能傳遞 List< Integer>,因此引入了通配符去解決對應的問題,能夠理解通配符是對泛型功能的擴展和加強。

  • extends 上限通配符 能夠接收extend後的類型及子類

  • super 下限通配符 能夠接收super後的類型及父類

向下邊這種寫法都是被禁止的

List<Number> list=new ArrayList<Integer>() //Error

List<Object> list;
List<String> strlist=new ArrayList<>();
list=strlist;							   //Error
複製代碼

首先明確兩個概念:

形參和實參

  • 形參 Type parameter

public class Shop< T>的那個T

表示我要建立一個 Shop 類,它的內部會用到一個統一的類型,這個類型姑且稱他爲 T 。

  • 實參 Type argument

其它地方尖括號裏的全是 Type argument,好比 Shop < Apple> appleShop;的 Apple ;

表示「那個統一代號,在這裏的類型我決定是這個」

其實用大白話來講形參就是定義泛型的地方,實參是傳入具體泛型類型的地方。

下邊以幾個例子來看看用法

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>();    //編譯錯誤
複製代碼
  1. x1使用了上限統配符,因此能夠接收Interger類型。
  2. x2中的String類型並不屬於Number及其子類,因此接收失敗報錯。
  3. y1使用了下限統配符,因此能夠接收Number類型。
  4. y2中的Byte類型並不屬於Integer及其父類,因此接收失敗報錯。
List<? extends Fruit> list = new ArrayList<>();
list.add(new Apple());//Error
list.get(0);//不報錯

List<? super Fruit> list2 = new ArrayList<>();
list2.add(new Apple());//不報錯
list2.get(0);//Error
複製代碼

當咱們使用上限通配符時對應方法參數中使用到泛型的方法將都沒法調用,由於咱們不能肯定具體傳入的是哪一種類型。

當咱們使用下限通配符時對應方法返回值中使用到泛型的方法將都沒法調用,由於咱們不能肯定具體返回的是哪一種類型。

這時咱們就有疑問了,既然使用通配符後對應的對象都處於報廢狀態,那麼這東西有啥用? 其實 ? 以及通配符一般都是用在方法參數中的,好比:

extends的例子

public int getTotalWeight(List<Fruit> list) {
    float totalWeight = 0;
    for (int i=0;i<list.size();i++) {
        Fruit fruit=list.get(i);
        totalWeight += fruit.getWeight();
    }
    return totalWeight;
}

List<Apple> listApple = new ArrayList<>();
listApple.add(new Apple());

List<Banana> listBanana = new ArrayList<>();
listBanana.add(new Banana());
int totalPrice=getTotalWeight(listApple)+getTotalWeight(listBanana);//編譯報錯
複製代碼

這種狀況是錯誤的,由於泛型不支持繼承,咱們是沒法直接傳入的。但咱們只要修改一下便可

public int getTotalWeight(List<? extends Fruit> list) {
    float totalWeight = 0;
    for (int i=0;i<list.size();i++) {
        Fruit fruit=list.get(i);
        totalWeight += fruit.getWeight();
    }
    return totalWeight;
}
複製代碼

這種狀況就OK了,由於這表明着咱們傳入的類型是可知的,上限通配符extends能夠接受extends右邊的類型及其子類。

super的例子

定義一個方法去添加自身

public class Apple extends Fruit{

    void addMeToList(List<Apple> list){
        list.add(this);
    }
}
複製代碼
List<Fruit> fruits = new ArrayList<>();
Apple apple=new Apple();
apple.addMeToList(fruits);//報錯
複製代碼

能夠看到當調用方法時會出現報錯,由於泛型不支持繼承。咱們修改一下

public class Apple implements Fruit{

    void addMeToList(List<? super Apple> list){
        list.add(this);
    }
}
複製代碼

這樣調用時就不會出現任何問題了。由於下限通配符能夠接收super類型後的父類,天然Apple的父類Fruit是確定能夠接收的。

進一步加深理解和認知

< T extends E> 和 <? extends E> 有什麼區別?

它們用的地方不同,< T extends E>只能用於形參(也就是泛型定義的時候),<? extends E>只能用於實參(也就是傳入具體泛型類型的時候)。 好比:

public void addAll(Bean<? extends E> bean;
public <T extends E> void addAll(Bean<T> bean;
複製代碼

下面程序合法嗎?

class Bean<T super Student> { //TODO }
複製代碼

編譯時報錯,由於super只能用做實參不能用於形參,extends實參形參均可以

下面兩個方法有什麼區別?爲何?

public static <T> T get1(T t1, T t2) {
    if(t1.compareTo(t2) >= 0);
    return t1;
}

public static <T extends Comparable> T get2(T t1, T t2){
    if(t1.compareTo(t2) >= 0);
    return t1;
}
複製代碼
  1. get1 方法直接編譯錯誤,由於編譯器在編譯前首先進行了泛型檢查和泛型擦除才編譯,因此等到真正編譯時 T 因爲沒有類型限定自動擦除爲 Object 類型,因此只能調用 Object 的方法,而 Object 沒有 compareTo 方法。
  2. get2 方法添加了泛型類型限定能夠正常使用,由於限定類型爲 Comparable 接口,其存在 compareTo 方法,因此 t一、t2 擦除後被強轉成功。因此類型限定在泛型類、泛型接口和泛型方法中均可以使用,不過無論該限定是類仍是接口都使用 extends 和 & 符號,若是限定類型既有接口也有類則類必須只有一個且放在首位,若是泛型類型變量有多個限定則原始類型就用第一個邊界的類型變量來替換。

總結通配符

  1. extends 方法參數中用到了泛型的方法都失效,返回值返回泛型的能夠調用。
  2. super 方法參數中用到了泛型的方法能夠調用,返回值返回泛型的都失效。 最直觀的例子就是List集合中使用了 <? extend 類型> 後對應的add方法都沒法調用,get方法能夠調用。super與之相反,List集合中使用了 <? super 類型> 後對應的get方法能夠調用,add方法都沒法調用。
相關文章
相關標籤/搜索