死磕Java泛型(一篇就夠)

Java泛型,算是一個比較容易產(chǎn)生誤解的知識點(diǎn),因?yàn)镴ava的泛型基于擦除實(shí)現(xiàn),在使用Java泛型時,往往會受到泛型實(shí)現(xiàn)機(jī)制的限制,如果不能深入全面的掌握泛型知識,就不能較好的駕馭使用泛型,同時在閱讀開源項(xiàng)目時也會處處碰壁,這一篇就帶大家全面深入的死磕Java泛型。

泛型擦除初探

相信泛型大家都使用過,所以一些基礎(chǔ)的知識點(diǎn)就不廢話了,以免顯得啰嗦。
先看下面的一小段代碼

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的方法,參數(shù)分別是List和List<Fruit>類型,這時候編譯器報錯了,并且很智能的給出了“ both methods has the same erasure” 這個錯誤提示。顯然,編譯器在抱怨,這二個方法具有同樣的簽名,嗯~~,這就是泛型擦除存在的一個證據(jù),要進(jìn)一步驗(yàn)證也很簡單。我們通過ByteCode Outline這個插件,可以很方便的查看類被編譯后的字節(jié)碼,這里我們只貼出eat方法的字節(jié)碼。

  // 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

可以看到參數(shù)確實(shí)已經(jīng)被擦除為List類型,這里要明確一點(diǎn)是,這里擦除的只是方法內(nèi)部的泛型信息,而泛型的元信息還是保存在類的class字節(jié)碼文件中,相信細(xì)心的同學(xué)已經(jīng)發(fā)現(xiàn)了上面我特意將方法的注釋一并貼了出來

 // signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V

這個signature字段大有玄機(jī),后面會詳細(xì)說明。
這里只是以泛型方法來做個說明,其實(shí)泛型類,泛型返回值都是類似的,兄弟們可以自己動手試試看。

為什么用擦除來實(shí)現(xiàn)泛型

要回答這個問題,需要知道泛型的歷史,Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器類等都是用Object來保證框架的靈活性,然后在讀取時強(qiáng)轉(zhuǎn)。但是這樣做有個很大的問題,那就是類型不安全,編譯器不能幫我們提前發(fā)現(xiàn)類型轉(zhuǎn)換錯誤,會將這個風(fēng)險帶到運(yùn)行時。
引入泛型,也就是為解決類型不安全的問題,但是由于當(dāng)時java已經(jīng)被廣泛使用,保證版本的向前兼容是必須的,所以為了兼容老版本jdk,泛型的設(shè)計者選擇了基于擦除的實(shí)現(xiàn)。

由于Java的泛型擦除,在運(yùn)行時,只有一個List類,那么相對于C#的基于膨脹的泛型實(shí)現(xiàn),Java類的數(shù)量相對較少,方法區(qū)占用的內(nèi)存就會小一點(diǎn),也算是一個額外的小優(yōu)點(diǎn)吧。

泛型擦除帶來的問題

由于泛型擦除,下面這些代碼都不能編譯通過

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

通配符

作為泛型擦除的補(bǔ)償,Java引入了通配符

List<? extends Fruit> fruitList;
List<? super Apple> appleList;

這二個通配符很多同學(xué)都存在誤解。

? extends

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

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

按照我們上面對? extends的理解,fruitList應(yīng)該是可以添加一個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. 因?yàn)? extends T給外界的承諾語義是,這個集合內(nèi)的元素都是T的子類型,但是到底是哪個子類型不知道,所以添加哪個子類型,編譯器都認(rèn)為是危險的,所以直接禁止添加。
  2. 因?yàn)? super T 給外界的承諾語義是,這個集合內(nèi)的元素的下界是T,所以向集合中添加T以及T的子類型是安全的,不會破壞這個承諾語義。
  3. List<Fruit>, List<Apple> 都是List<? super Apple>的子類型。
    List<Apple> 是List<? extends Apple>的子類型。

關(guān)于泛型的使用,Jdk中有很多經(jīng)典的應(yīng)用范例,比如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字節(jié)碼中會有個signature字段來保存泛型信息。我們新建一個泛型方法

    public <T extends Apple> T plant(T fruit) {
        return fruit;
    }

查看class文件的二進(jìn)制信息,發(fā)現(xiàn)里面確實(shí)有Signature字段信息。

Signature?%<T:Lgeneric/FruitKata$Apple;>(TT;)TT;

既然泛型信息還是在class文件中,那我們有沒有辦法在運(yùn)行時拿到呢?
辦法肯定是有的。
來看一個例子

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

// 打印結(jié)果
class java.lang.String
class generic.FruitKata$Apple

可以看到我們拿到并打印了泛型的原始類型信息。為了加深對泛型使用的理解,我接下來再看幾個小例子。

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

這是一段很簡單的Gson解析使用代碼,我們進(jìn)一步去看它fromJson的方法實(shí)現(xiàn)

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

最終會執(zhí)行到

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

通過我們傳入的Class類型構(gòu)造TypeToken,然后通過TypeAdapter將json字符串轉(zhuǎn)化為對象T,中間的細(xì)節(jié)這里就不繼續(xù)深入了。

泛型在retrofit中的使用

我們在使用retrofit時,一般都會定義一個或多個ApiService接口類

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

接口方法的返回值都使用了泛型,所以注定在編譯期是要被擦除的,那retrofit是如何得到原始泛型信息的呢。其實(shí)有上面的泛型知識以及Gson的使用說明,相信大家以及有答案了。
retrofit框架本身設(shè)計的很優(yōu)雅,細(xì)節(jié)這里我們不深入展開,這里我們只關(guān)心泛型數(shù)據(jù)轉(zhuǎn)換為返回值的過程。
我們需要定義如下幾個類

// 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;
    }
}

接下來,我定義一個動態(tài)代理,

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數(shù)據(jù)
public static String mockAppleJsonString() {
   List<Apple> apples = new ArrayList<>();
   apples.add(new Apple(1, "紅富士"));
   apples.add(new Apple(2, "青蘋果"));
   return new Gson().toJson(apples);
}

接下來就是正常的調(diào)用了,這里模擬了retrofit數(shù)據(jù)轉(zhuǎn)換的過程。

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

// 輸出結(jié)果
color:1; name:紅富士
color:2; name:青蘋果
泛型在MVP中的應(yīng)用

MVP模式相信做Android開發(fā)的沒人不知道,假設(shè)我們有這樣幾個類

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

由于泛型擦除的關(guān)系,我們不能在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,并調(diào)用bindView來綁定我們的視圖接口。通過這種方式,我們利用泛型的能力,基類包辦了所有的初始化任務(wù),不但邏輯簡單,而且也體現(xiàn)了高內(nèi)聚,在實(shí)際項(xiàng)目中可以嘗試使用。

總結(jié)

深入理解Java泛型是工程師進(jìn)階的必備技能,希望你看了這篇文章,在今后,不論是面試還是其他的時候,談到Java泛型時都能夠云淡風(fēng)輕,在使用泛型編寫代碼時也能夠信手拈來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • “泛型”這個術(shù)語的意思是:"適用于許多許多的類型”。如何做到這一點(diǎn)呢,正是通過解耦類或方法與所使用的類型之間的約束...
    王偵閱讀 1,348評論 0 0
  • 參數(shù)類型的好處 在 Java 引入泛型之前,泛型程序設(shè)計是用繼承實(shí)現(xiàn)的。ArrayList 類只維護(hù)一個 Obje...
    杰哥長得帥閱讀 951評論 0 3
  • 1.泛型的由來 一般的類和方法,只能使用具體的類型,要么是基本數(shù)據(jù)類型,要么是自定義的類型,如果要編寫可以適用于多...
    BigDreamMaker閱讀 633評論 0 1
  • 泛型程序設(shè)計 泛型程序設(shè)計意味著編寫的代碼可以被很多不同類型的對象所重用。例如ArrayList類可以聚集任何類型...
    Steven1997閱讀 769評論 1 0
  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符,讓我看了幾遍《Java...
    珞澤珈群閱讀 8,120評論 12 51

友情鏈接更多精彩內(nèi)容