1. 程式人生 > >【讀書筆記】泛型深究

【讀書筆記】泛型深究

【讀書筆記】泛型深究

筆記連結

引子

  • 一道經典的測試題
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());
  • 不瞭解泛型的和很熟悉泛型的同學應該能夠答出來,而對泛型有所瞭解,但是瞭解不深入的同學可能會答錯。正確答案是 true。上面的程式碼中涉及到了泛型,而輸出的結果緣由是型別擦除

泛型類與泛型方法的共存現象

public class Test1<T>{

   public  void testMethod(T t){
       System.out.println(t.getClass().getName());
   }
   public  <T> T testMethod1(T t){
       return t;
   }
}
  • 上面程式碼中,Test1 是泛型類,testMethod 是泛型類中的普通方法,而 testMethod1 是一個泛型方法。而泛型類中的型別引數與泛型方法中的型別引數是沒有相應的聯絡的
    ,泛型方法始終以自己定義的型別引數為準。
  • 測試程式碼
Test1<String> t = new Test1();
t.testMethod("generic");//必須填字串
Integer i = t.testMethod1(new Integer(1));//字串 or 數字等 均可
  • 泛型類的實際型別引數是 String,而傳遞給泛型方法的型別引數是 Integer,兩者不相干。
  • 為了避免混淆,程式碼可以更改為這樣
public class Test1<T>{
   public  void testMethod(T t){
       System.out.println(t.getClass().getName());
   }
   public  <E> E testMethod1(E e){
       return e;
   }
}

T 與 ?

  • 已經有了 <T> 的形式了,為什麼還要引進 <?> 這樣的概念呢?
class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;
  • Base 是 Sub 的父類,它們之間是繼承關係,所以 Sub 的例項可以給一個 Base 引用賦值,那麼
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
  • 最後一行程式碼,編譯器不會讓它通過的。Sub 是 Base 的子類,不代表 List<Sub> 和 List<Base> 有繼承關係。
  • 但是,在現實編碼中,確實有這樣的需求,希望泛型能夠處理某一範圍內的資料型別,比如某個類和它的子類,對此 Java 引入了萬用字元?這個概念。
  • 所以,萬用字元的出現是為了指定泛型中的類型範圍
    • <?> 被稱作無限定的萬用字元。
    • <? extends T> 被稱作有上限的萬用字元。
    • <? super T> 被稱作有下限的萬用字元。

<?>

public void testWildCards(Collection<?> collection){
}
  • 上面的程式碼隱略地表達了一個意圖: testWidlCards() 這個方法內部無需關注 Collection 中的真實型別,因為它是未知的。所以,你只能呼叫 Collection 中與型別無關的方法。
  • 當 <?> 存在時,Collection 物件喪失了 add() 方法的功能,編譯器不通過。
List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 編譯不通過
  • <?> 提供了只讀的功能,也就是它刪減了增加具體型別元素的能力,只保留與具體型別無關的功能。它不管裝載在這個容器內的元素是什麼型別,它只關心元素的數量、容器是否為空。這種需求還是很常見的。

<? extends T>

  • <?> 代表著型別未知,但是我們的確需要對於型別的描述再精確一點,我們希望在一個範圍內確定類別,比如型別 A 及 型別 A 的子類都可以。
public void testSub(Collection<? extends Base> para){

}
  • 上面程式碼中,para 這個 Collection 接受 Base 及 Base 的子類的型別。
  • 但是,它仍然喪失了寫操作的能力。也就是說
para.add(new Sub());// 編譯不通過
para.add(new Base());// 編譯不通過
  • 沒有關係,我們不知道具體型別,但是我們至少清楚了型別的範圍。

<? super T>

  • 這個和 <? extends T> 相對應,代表 T 及 T 的超類。
public void testSuper(Collection<? super Sub> para){
}
  • <? super T> 神奇的地方在於,它擁有一定程度的寫操作的能力
public void testSuper(Collection<? super Sub> para){
   para.add(new Sub());//編譯通過
   para.add(new Base());//編譯不通過
}

型別引數T與萬用字元?的區別

  • 一般而言,? 能幹的事情都可以用T替換。
public void testWildCards(Collection<?> collection){}

可以替換為

public <T> void test(Collection<T> collection){}
  • 值得注意的是,如果用泛型方法來取代萬用字元,那麼上面程式碼中 collection 是能夠進行寫操作的。只不過要進行強制轉換。
public <T> void test(Collection<T> collection){
   collection.add((T)new Integer(12));
   collection.add((T)"123");
}
  • 需要特別注意的是,型別引數適用於引數之間的類別依賴關係,舉例說明。
public class Test2 <T,E extends T>{
   T value1;
   E value2;
}
public <D,S extends D> void test(D d,S s){

}
  • E 型別是 T 型別的子類,顯然這種情況型別引數更適合。
  • 有一種情況是,萬用字元和型別引數一起使用。
public <T> void test(T t,Collection<? extends T> collection){

}
  • 如果一個方法的返回型別依賴於引數的型別,那麼萬用字元也無能為力。
public T test1(T t){
   return value1;
}

型別擦除

  • 泛型是 Java 1.5 版本才引進的概念,在這之前是沒有泛型的概念的,但顯然,泛型程式碼能夠很好地和之前版本的程式碼很好地相容。 這是因為,泛型資訊只存在於程式碼編譯階段,在進入 JVM 之前,與泛型相關的資訊會被擦除掉,專業術語叫做型別擦除
  • 通俗地講,泛型類和普通類在 java 虛擬機器內是沒有什麼特別的地方。
  • 回顧文章開始時的那段程式碼
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());

  • 列印的結果為 true 是因為 List 和 List 在 jvm 中的 Class 都是 List.class。泛型資訊被擦除了。
  • 可能同學會問,那麼型別 String 和 Integer 怎麼辦?答案是泛型轉譯。
public class Erasure <T>{
   T object;
   public Erasure(T object) {
       this.object = object;
   }
}
  • Erasure 是一個泛型類,我們檢視它在執行時的狀態資訊可以通過反射。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
  • 列印結果:erasure class is:com.frank.test.Erasure
  • Class 的型別仍然是 Erasure 並不是 Erasure<T> 這種形式,那我們再看看泛型類中 T 的型別在 jvm 中是什麼具體型別。
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
   System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
  • 列印結果:Field name object type:java.lang.Object
  • 那我們可不可以說,泛型類被型別擦除後,相應的型別就被替換成 Object 型別呢?這種說法,不完全正確。
  • 我們更改一下程式碼
public class Erasure <T extends String>{
//  public class Erasure <T>{
   T object;

   public Erasure(T object) {
       this.object = object;
   }

}
  • 再看測試結果:Field name object type:java.lang.String
  • 現在可以下結論了:在泛型類被型別擦除的時候,之前泛型類中的型別引數部分如果沒有指定上限,如 <T> 則會被轉譯成普通的 Object 型別,如果指定了上限如 <T extends String> 則型別引數就被替換成型別上限。
  • 所以,在反射中。
public class Erasure <T>{
   T object;

   public Erasure(T object) {
       this.object = object;
   }

   public void add(T object){

   }

}
  • add() 這個方法對應的 Method 的簽名應該是 Object.class
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
   System.out.println(" method:"+m.toString());
}
  • 列印結果:method:public void com.frank.test.Erasure.add(java.lang.Object)
  • 也就是說,如果你要在反射中找到 add 對應的 Method,你應該呼叫 getDeclaredMethod(“add”,Object.class) 否則程式會報錯,提示沒有這麼一個方法,原因就是型別擦除的時候,T 被替換成 Object 型別了。

型別擦除帶來的侷限性

  • 型別擦除,是泛型能夠與之前的 java 版本程式碼相容共存的原因。但也因為型別擦除,它會抹掉很多繼承相關的特性,這是它帶來的侷限性。
  • 理解型別擦除有利於我們繞過開發當中可能遇到的雷區,同樣理解型別擦除也能讓我們繞過泛型本身的一些限制。比如
List<Integer> list = new ArrayList<>();
list.add(1);
list.add("test");//編譯不過
  • 正常情況下,因為泛型的限制,編譯器不讓最後一行程式碼編譯通過,因為類似不匹配,但是,基於對型別擦除的瞭解,利用反射,我們可以繞過這個限制。
  • List原始碼如下
public interface List<E> extends Collection<E>{

    boolean add(E e);
}
  • 因為 E 代表任意的型別,所以型別擦除時,add 方法其實等同於
boolean add(Object obj);
  • 那麼,利用反射,我們繞過編譯器去呼叫 add 方法。
List<Integer> list = new ArrayList<>();
list.add(1);
//list.add("test");//編譯不過
//list.add(42.9f);//編譯不過
try {
  Method method = list.getClass().getDeclaredMethod("add",Object.class);
  method.invoke(list,"test");
  method.invoke(list,42.9f);
} catch (NoSuchMethodException e) {
  e.printStackTrace();
} catch (IllegalAccessException e) {
  e.printStackTrace();
} catch (InvocationTargetException e) {
  e.printStackTrace();
}

for(Object o:list){//Object o
  System.out.println(o);
}
  • 可以看到,利用型別擦除的原理,用反射的手段就繞過了正常開發中編譯器不允許的操作限制

不能建立具體型別的泛型陣列

List<Integer>[] li1 = new ArrayList<Integer>[10];//編譯不過,提示generic array creation
List<Boolean>[] li2 = new ArrayList<Boolean>[10];//編譯不過,提示generic array creation
//替代方案 建立 ArrayList<ArrayList<String>>
//List<String>[] li3 = new ArrayList[10];//編譯通過why?
  • 原因還是型別擦除帶來的影響。
  • List<Integer> 和 List<Boolean> 在 jvm 中等同於List<Object> ,所有的型別資訊都被擦除,程式也無法分辨一個數組中的元素型別具體是 List<Integer>型別還是 List<Boolean> 型別。
  • 但是下面程式碼可以編譯通過
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
  • 藉助於無限定萬用字元卻可以,前面講過 ? 代表未知型別,所以它涉及的操作都基本上與型別無關,因此 jvm 不需要針對它對型別作判斷,因此它能編譯通過,但是,只提供了陣列中的元素因為萬用字元原因,它只能讀,不能寫。比如,上面的 v 這個區域性變數,它只能進行 get() 操作,不能進行 add() 操作。
  • 在java中,不能通過直接通過T[] tarr=new T[10]的方式來建立陣列,最簡單的方式便是通過Array.newInstance(Classtype,int size)的方式來建立陣列