死磕Java泛型(一篇就夠)

Java泛型,算是一個比較容易產生誤解的知識點,由於Java的泛型基於擦除實現,在使用Java泛型時,每每會受到泛型實現機制的限制,若是不能深刻全面的掌握泛型知識,就不能較好的駕馭使用泛型,同時在閱讀開源項目時也會到處碰壁,這一篇就帶你們全面深刻的死磕Java泛型。java

泛型擦除初探

相信泛型你們都使用過,因此一些基礎的知識點就不廢話了,以避免顯得囉嗦。 先看下面的一小段代碼面試

public class FruitKata {
    class Fruit {}
    class Apple extends generic.Fruit {}
    
    public void eat(List fruitList) {}

    public void eat(List<Fruit> fruitList) { }   // error, both methods has the same erasure
}
複製代碼

咱們在FruitKata類中定義了二個eat的方法,參數分別是List和List<> 類型,這時候編譯器報錯了,而且很智能的給出了「 both methods has the same erasure」 這個錯誤提示。顯然,編譯器在抱怨,這二個方法具備一樣的簽名,嗯~~,這就是泛型擦除存在的一個證據,要進一步驗證也很簡單。咱們經過ByteCode Outline這個插件,能夠很方便的查看類被編譯後的字節碼,這裏咱們只貼出eat方法的字節碼。json

// access flags 0x1
  // signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
  // declaration: void eat(java.util.List<generic.FruitKata$Fruit>)
  public eat(Ljava/util/List;)V
複製代碼

能夠看到參數確實已經被擦除爲List類型,這裏要明確一點是,這裏擦除的只是方法內部的泛型信息,而泛型的元信息仍是保存在類的class字節碼文件中,相信細心的同窗已經發現了上面我特地將方法的註釋一併貼了出來api

// signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
複製代碼

這個signature字段大有玄機,後面會詳細說明。 這裏只是以泛型方法來作個說明,其實泛型類,泛型返回值都是相似的,兄弟們能夠本身動手試試看。安全

爲何用擦除來實現泛型

要回答這個問題,須要知道泛型的歷史,Java的泛型是在Jdk 1.5 引入的,在此以前Jdk中的容器類等都是用Object來保證框架的靈活性,而後在讀取時強轉。可是這樣作有個很大的問題,那就是類型不安全,編譯器不能幫咱們提早發現類型轉換錯誤,會將這個風險帶到運行時。 引入泛型,也就是爲解決類型不安全的問題,可是因爲當時java已經被普遍使用,保證版本的向前兼容是必須的,因此爲了兼容老版本jdk,泛型的設計者選擇了基於擦除的實現。 因爲Java的泛型擦除,在運行時,只有一個List類,那麼相對於C#的基於膨脹的泛型實現,Java類的數量相對較少,方法區佔用的內存就會小一點,也算是一個額外的小優勢吧。bash

泛型擦除帶來的問題

因爲泛型擦除,下面這些代碼都不能編譯經過app

T t = new T();
T[] arr = new T[10];
List<T> list = new ArrayList<T>();
T instanceof Object
複製代碼

通配符

做爲泛型擦除的補償,Java引入了通配符框架

List<? extends Fruit> fruitList;
List<? super Apple> appleList;
複製代碼

這二個通配符不少同窗都存在誤解。dom

? extends

?extends Fruit 表示Fruit是這個傳入的泛型的基類(Fruit是泛型的上界),仍是以上面的Fruit和Apple爲例,看下面這段代碼ide

List<? extends Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit());  //error
複製代碼

按照咱們上面對? extends的理解,fruitList應該是能夠添加一個Fruit的,可是編譯器卻給咱們報錯了。我第一次看到這裏時也感受不太好理解,咱們來看個例子就能理解了。

List<? extends Fruit>  fruitList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
fruitList = appleList;
fruitList.add(new Fruit());   //error
複製代碼

若是fruitList容許添加Fruit,咱們就將Fruit添加到了AppleList中了,這確定是不能接受的。

? super

再來看個?super的例子

List<? super Apple> superAppleList = new ArrayList<>();
superAppleList.add(new Apple());
superAppleList.add(new Fruit());  // error
複製代碼

向superAppleList中添加Apple是能夠的,添加Fruit仍是會報錯,好,上面咱們說的這些就是 PECS 原則。

PECS

英文全稱,Producer Extends Consumer Super,

  1. 若是須要一個只讀的泛型集合,使用?extends T
  2. 若是須要一個只寫的泛型集合,使用?super T

我本身是這樣來理解通配符的

  1. 由於? extends T給外界的承諾語義是,這個集合內的元素都是T的子類型,可是究竟是哪一個子類型不知道,因此添加哪一個子類型,編譯器都認爲是危險的,因此直接禁止添加。
  2. 由於? super T 給外界的承諾語義是,這個集合內的元素的下界是T,因此向集合中添加T以及T的子類型是安全的,不會破壞這個承諾語義。
  3. List, List 都是List<? super Apple>的子類型。 List 是List<? extends Apple>的子類型。

關於泛型的使用,Jdk中有不少經典的應用範例,好比Collections的copy方法

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }
複製代碼

泛型擦除了,咱們還能拿到泛型信息嗎

前面咱們提到過class字節碼中會有個signature字段來保存泛型信息。咱們新建一個泛型方法

public <T extends Apple> T plant(T fruit) {
        return fruit;
    }
複製代碼

查看class文件的二進制信息,發現裏面確實有Signature字段信息。

Signature%<T:Lgeneric/FruitKata$Apple;>(TT;)TT;
複製代碼

既然泛型信息仍是在class文件中,那咱們有沒有辦法在運行時拿到呢? 辦法確定是有的。 來看一個例子

Class clazz = HashMap<String, Apple>(){}.getClass();
  Type superType = clazz.getGenericSuperclass();
  if (superType instanceof ParameterizedType) {
  ParameterizedType parameterizedType = (ParameterizedType) superType;
  Type[] actualTypes = parameterizedType.getActualTypeArguments();
   for (Type type : actualTypes) {
            System.out.println(type);
       }
   }

// 打印結果
class java.lang.String
class generic.FruitKata$Apple
複製代碼

能夠看到咱們拿到並打印了泛型的原始類型信息。爲了加深對泛型使用的理解,我接下來再看幾個小例子。

泛型在Gson解析中的使用
String jsonString = ".....";  // 這裏省略json字符串
Apple apple = new Gson().fromJson(jsonString, Apple.class);
複製代碼

這是一段很簡單的Gson解析使用代碼,咱們進一步去看它fromJson的方法實現

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }
複製代碼

最終會執行到

TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
  TypeAdapter<T> typeAdapter = getAdapter(typeToken);
  T object = typeAdapter.read(reader);
複製代碼

經過咱們傳入的Class類型構造TypeToken,而後經過TypeAdapter將json字符串轉化爲對象T,中間的細節這裏就不繼續深刻了。

泛型在retrofit中的使用

咱們在使用retrofit時,通常都會定義一個或多個ApiService接口類

@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
複製代碼

接口方法的返回值都使用了泛型,因此註定在編譯期是要被擦除的,那retrofit是如何獲得原始泛型信息的呢。其實有上面的泛型知識以及Gson的使用說明,相信你們以及有答案了。 retrofit框架自己設計的很優雅,細節這裏咱們不深刻展開,這裏咱們只關心泛型數據轉換爲返回值的過程。 咱們須要定義以下幾個類

// ApiService.class
public interface ApiService {
    Observable<List<Apple>> getAppleList();
}

// Apple.class
class Apple extends Fruit {
    private int color;
    private String name;
    public Apple() {}

    public Apple(int color, String name) {
        this.color = color;
        this.name = name;
    }

    @Override
    public String toString() {
        return "color:" + this.color + "; name:" + name;
    }
}

複製代碼

接下來,我定義一個動態代理,

InvocationHandler handler = new InvocationHandler() {
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof ParameterizedType) {
               ParameterizedType parameterizedType = (ParameterizedType) returnType;
               Type[] types = parameterizedType.getActualTypeArguments();
               if (types.length > 0) {
                   Type type = types[0];
                   Object object = new Gson().fromJson(mockAppleJsonString(), type);
                   return Observable.just(object);
             }
           }
          return null;
     }
  };

// mock json數據
public static String mockAppleJsonString() {
   List<Apple> apples = new ArrayList<>();
   apples.add(new Apple(1, "紅富士"));
   apples.add(new Apple(2, "青蘋果"));
   return new Gson().toJson(apples);
}
複製代碼

接下來就是正常的調用了,這裏模擬了retrofit數據轉換的過程。

ApiService apiService = (ApiService) Proxy.newProxyInstance(ProxyKata.class.getClassLoader(),
                new Class[] {ApiService.class}, handler);

Observable<List<Apple>> call = apiService.getAppleList();
if (call != null) {
      call.subscribe(apples -> {
           if (apples != null) {
              for (Apple apple : apples) {
                 System.out.println(apple);
              }
         }
     });
}

// 輸出結果
color:1; name:紅富士
color:2; name:青蘋果
複製代碼
泛型在MVP中的應用

MVP模式相信作Android開發的沒人不知道,假設咱們有這樣幾個類

public class BaseActivity<V extends IView, P extends IPresenter<V>> extends AppCompatActivity {
   protected P mPresenter;
  //....
}
public class MainActivity extends BaseActivity<MainView, MainPresenter> implements MainView {
  //....
}
複製代碼

因爲泛型擦除的關係,咱們不能在BaseActivity中直接新建Presenter來初始化mPresenter,因此通常一般的作法是暴露一個createPresenter方法讓子類重寫。可是今天咱們介紹另一種方法,直接看代碼

// BaseActivity.class
        Type superType = getClass().getGenericSuperclass();
        if (superType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superType;
            Type[] types = parameterizedType.getActualTypeArguments();
            for (Type type : types) {
                if (type instanceof Class) {
                    Class clazz = (Class) type;
                    try {
                        mPresenter = (P) clazz.newInstance();
                        mPresenter.bindView((V) this);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
複製代碼

咱們經過在BaseActivity中是可以拿到泛型的原始信息的,經過反射初始化出來mPresenter,並調用bindView來綁定咱們的視圖接口。經過這種方式,咱們利用泛型的能力,基類包辦了全部的初始化任務,不但邏輯簡單,並且也體現了高內聚,在實際項目中能夠嘗試使用。

總結

深刻理解Java泛型是工程師進階的必備技能,但願你看了這篇文章,在從此,不管是面試仍是其餘的時候,談到Java泛型時都可以雲淡風輕,在使用泛型編寫代碼時也可以信手拈來。

相關文章
相關標籤/搜索