Java泛型,算是一個(gè)比較容易產(chǎn)生誤解的知識(shí)點(diǎn),因?yàn)镴ava的泛型基于擦除實(shí)現(xiàn),在使用Java泛型時(shí),往往會(huì)受到泛型實(shí)現(xiàn)機(jī)制的限制,如果不能深入全面的掌握泛型知識(shí),就不能較好的駕馭使用泛型,同時(shí)在閱讀開(kāi)源項(xiàng)目時(shí)也會(huì)處處碰壁,這一篇就帶大家全面深入的死磕Java泛型。
泛型擦除初探
相信泛型大家都使用過(guò),所以一些基礎(chǔ)的知識(shí)點(diǎn)就不廢話(huà)了,以免顯得啰嗦。
先看下面的一小段代碼
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
}
我們?cè)贔ruitKata類(lèi)中定義了二個(gè)eat的方法,參數(shù)分別是List和List<Fruit>類(lèi)型,這時(shí)候編譯器報(bào)錯(cuò)了,并且很智能的給出了“ both methods has the same erasure” 這個(gè)錯(cuò)誤提示。顯然,編譯器在抱怨,這二個(gè)方法具有同樣的簽名,嗯~~,這就是泛型擦除存在的一個(gè)證據(jù),要進(jìn)一步驗(yàn)證也很簡(jiǎn)單。我們通過(guò)ByteCode Outline這個(gè)插件,可以很方便的查看類(lèi)被編譯后的字節(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)被擦除為L(zhǎng)ist類(lèi)型,這里要明確一點(diǎn)是,這里擦除的只是方法內(nèi)部的泛型信息,而泛型的元信息還是保存在類(lèi)的class字節(jié)碼文件中,相信細(xì)心的同學(xué)已經(jīng)發(fā)現(xiàn)了上面我特意將方法的注釋一并貼了出來(lái)
// signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
這個(gè)signature字段大有玄機(jī),后面會(huì)詳細(xì)說(shuō)明。
這里只是以泛型方法來(lái)做個(gè)說(shuō)明,其實(shí)泛型類(lèi),泛型返回值都是類(lèi)似的,兄弟們可以自己動(dòng)手試試看。
為什么用擦除來(lái)實(shí)現(xiàn)泛型
要回答這個(gè)問(wèn)題,需要知道泛型的歷史,Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器類(lèi)等都是用Object來(lái)保證框架的靈活性,然后在讀取時(shí)強(qiáng)轉(zhuǎn)。但是這樣做有個(gè)很大的問(wèn)題,那就是類(lèi)型不安全,編譯器不能幫我們提前發(fā)現(xiàn)類(lèi)型轉(zhuǎn)換錯(cuò)誤,會(huì)將這個(gè)風(fēng)險(xiǎn)帶到運(yùn)行時(shí)。
引入泛型,也就是為解決類(lèi)型不安全的問(wèn)題,但是由于當(dāng)時(shí)java已經(jīng)被廣泛使用,保證版本的向前兼容是必須的,所以為了兼容老版本jdk,泛型的設(shè)計(jì)者選擇了基于擦除的實(shí)現(xiàn)。
由于Java的泛型擦除,在運(yùn)行時(shí),只有一個(gè)List類(lèi),那么相對(duì)于C#的基于膨脹的泛型實(shí)現(xiàn),Java類(lèi)的數(shù)量相對(duì)較少,方法區(qū)占用的內(nèi)存就會(huì)小一點(diǎn),也算是一個(gè)額外的小優(yōu)點(diǎn)吧。
泛型擦除帶來(lái)的問(wèn)題
由于泛型擦除,下面這些代碼都不能編譯通過(guò)
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;
這二個(gè)通配符很多同學(xué)都存在誤解。
? extends
?extends Fruit 表示Fruit是這個(gè)傳入的泛型的基類(lèi)(Fruit是泛型的上界),還是以上面的Fruit和Apple為例,看下面這段代碼
List<? extends Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit()); //error
按照我們上面對(duì)? extends的理解,fruitList應(yīng)該是可以添加一個(gè)Fruit的,但是編譯器卻給我們報(bào)錯(cuò)了。我第一次看到這里時(shí)也感覺(jué)不太好理解,我們來(lái)看個(gè)例子就能理解了。
List<? extends Fruit> fruitList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
fruitList = appleList;
fruitList.add(new Fruit()); //error
如果fruitList允許添加Fruit,我們就將Fruit添加到了AppleList中了,這肯定是不能接受的。
? super
再來(lái)看個(gè)?super的例子
List<? super Apple> superAppleList = new ArrayList<>();
superAppleList.add(new Apple());
superAppleList.add(new Fruit()); // error
向superAppleList中添加Apple是可以的,添加Fruit還是會(huì)報(bào)錯(cuò),好,上面我們說(shuō)的這些就是 PECS 原則。
PECS
英文全稱(chēng),Producer Extends Consumer Super,
- 如果需要一個(gè)只讀的泛型集合,使用?extends T
- 如果需要一個(gè)只寫(xiě)的泛型集合,使用?super T
我自己是這樣來(lái)理解通配符的
- 因?yàn)? extends T給外界的承諾語(yǔ)義是,這個(gè)集合內(nèi)的元素都是T的子類(lèi)型,但是到底是哪個(gè)子類(lèi)型不知道,所以添加哪個(gè)子類(lèi)型,編譯器都認(rèn)為是危險(xiǎn)的,所以直接禁止添加。
- 因?yàn)? super T 給外界的承諾語(yǔ)義是,這個(gè)集合內(nèi)的元素的下界是T,所以向集合中添加T以及T的子類(lèi)型是安全的,不會(huì)破壞這個(gè)承諾語(yǔ)義。
- List<Fruit>, List<Apple> 都是List<? super Apple>的子類(lèi)型。
List<Apple> 是List<? extends Apple>的子類(lèi)型。
關(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());
}
}
}
泛型擦除了,我們還能拿到泛型信息嗎
前面我們提到過(guò)class字節(jié)碼中會(huì)有個(gè)signature字段來(lái)保存泛型信息。我們新建一個(gè)泛型方法
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文件中,那我們有沒(méi)有辦法在運(yùn)行時(shí)拿到呢?
辦法肯定是有的。
來(lái)看一個(gè)例子
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
可以看到我們拿到并打印了泛型的原始類(lèi)型信息。為了加深對(duì)泛型使用的理解,我接下來(lái)再看幾個(gè)小例子。
泛型在Gson解析中的使用
String jsonString = "....."; // 這里省略json字符串
Apple apple = new Gson().fromJson(jsonString, Apple.class);
這是一段很簡(jiǎn)單的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);
}
最終會(huì)執(zhí)行到
TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
TypeAdapter<T> typeAdapter = getAdapter(typeToken);
T object = typeAdapter.read(reader);
通過(guò)我們傳入的Class類(lèi)型構(gòu)造TypeToken,然后通過(guò)TypeAdapter將json字符串轉(zhuǎn)化為對(duì)象T,中間的細(xì)節(jié)這里就不繼續(xù)深入了。
泛型在retrofit中的使用
我們?cè)谑褂胷etrofit時(shí),一般都會(huì)定義一個(gè)或多個(gè)ApiService接口類(lèi)
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
接口方法的返回值都使用了泛型,所以注定在編譯期是要被擦除的,那retrofit是如何得到原始泛型信息的呢。其實(shí)有上面的泛型知識(shí)以及Gson的使用說(shuō)明,相信大家以及有答案了。
retrofit框架本身設(shè)計(jì)的很優(yōu)雅,細(xì)節(jié)這里我們不深入展開(kāi),這里我們只關(guān)心泛型數(shù)據(jù)轉(zhuǎn)換為返回值的過(guò)程。
我們需要定義如下幾個(gè)類(lèi)
// 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;
}
}
接下來(lái),我定義一個(gè)動(dòng)態(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, "青蘋(píng)果"));
return new Gson().toJson(apples);
}
接下來(lái)就是正常的調(diào)用了,這里模擬了retrofit數(shù)據(jù)轉(zhuǎn)換的過(guò)程。
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:青蘋(píng)果
泛型在MVP中的應(yīng)用
MVP模式相信做Android開(kāi)發(fā)的沒(méi)人不知道,假設(shè)我們有這樣幾個(gè)類(lèi)
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來(lái)初始化mPresenter,所以一般通常的做法是暴露一個(gè)createPresenter方法讓子類(lèi)重寫(xiě)。但是今天我們介紹另外一種方法,直接看代碼
// 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();
}
}
}
}
我們通過(guò)在BaseActivity中是能夠拿到泛型的原始信息的,通過(guò)反射初始化出來(lái)mPresenter,并調(diào)用bindView來(lái)綁定我們的視圖接口。通過(guò)這種方式,我們利用泛型的能力,基類(lèi)包辦了所有的初始化任務(wù),不但邏輯簡(jiǎn)單,而且也體現(xiàn)了高內(nèi)聚,在實(shí)際項(xiàng)目中可以嘗試使用。
總結(jié)
深入理解Java泛型是工程師進(jìn)階的必備技能,希望你看了這篇文章,在今后,不論是面試還是其他的時(shí)候,談到Java泛型時(shí)都能夠云淡風(fēng)輕,在使用泛型編寫(xiě)代碼時(shí)也能夠信手拈來(lái)。