反射和泛型的這些坑,你踏空了嗎?
1 當(dāng)反射遇到方法重載
重載grade方法,入?yún)⒎謩e是int和Integer。
不通過反射,選用哪個重載方法很清晰,比如傳入666走int參數(shù)重載方法,傳入Integer.valueOf(“666”)走Integer重載。
但你若墨守成規(guī)認(rèn)為反射調(diào)用方法也是根據(jù)入?yún)㈩愋痛_定方法重載。
使用getDeclaredMethod獲取 grade方法,然后傳入Integer.valueOf(“36”)
因為通過反射進行方法調(diào)用都是
通過方法簽名來確定方法
本例的getDeclaredMethod傳入的參數(shù)類型Integer.TYPE其實一直代表int
所以實際執(zhí)行方法時不管傳包裝類型/基本類型,最終都是調(diào)用int入?yún)⒌膅rade方法。
將Integer.TYPE改為Integer.class,實際執(zhí)行的參數(shù)類型就是Integer了。且無論傳包裝類型/基本類型,最終都會調(diào)用Integer為入?yún)⒌膅rade方法。
所以反射調(diào)用方法,是以反射獲取方法時傳入的方法名和參數(shù)類型來確定調(diào)用的方法。
2 泛型經(jīng)過類型擦除多出橋接方法的坑
泛型是一種編程范式,允許開發(fā)者使用類型參數(shù)替代精確類型,實例化時再指明具體類型。也利于代碼重用,將一套代碼應(yīng)用到多種數(shù)據(jù)類型。
泛型的類型檢測,可以在編譯時暴露大多數(shù)泛型編碼錯誤。但由于歷史兼容性而妥協(xié)的泛型類型擦除,在運行時才會暴露很多坑。
案例
期望在類字段內(nèi)容變動時記錄日志,于是開發(fā)同學(xué)就想到定義一個泛型父類,并在父類中定義一個統(tǒng)一的日志記錄方法,子類可繼承該方法。上線后總出現(xiàn)日志重復(fù)記錄問題。
父類
子類Child1 未提供父類泛型參數(shù)且定義了一個參數(shù)為String而非T的setValue。期望覆蓋父類的setValue實現(xiàn)。
子類方法的調(diào)用是通過反射。
雖Parent的value字段正確設(shè)置JavaEdge,但父類setValue調(diào)用了兩次,計數(shù)器而顯示2
兩次Parent的setValue方法調(diào)用,是因為getMethods找到了兩個setValue的,分屬于父類/子類。
子類重寫父類方法失敗原因
子類未指定String泛型參數(shù),父類的泛型方法setValue(T value)泛型擦除后是setValue(Object value),于是子類入?yún)tring的setValue被當(dāng)作了新方法
子類的setValue方法未加@Override注解,編譯器未能檢測到重寫失敗。
重寫子類方法時,務(wù)必使用@Override注解。
但有人認(rèn)為問題是反射API使用不當(dāng)而未意識到重寫失敗。查文檔后才發(fā)現(xiàn)
getMethods能獲得當(dāng)前類和父類的所有public方法
getDeclaredMethods僅獲得當(dāng)前類所有的public、protected、package和private方法
于是用getDeclaredMethods替換getMethods:
這雖能解決重復(fù)記錄日志,但未解決子類重寫父類方法失敗,日志:
當(dāng)其他人使用Child1時還是會發(fā)現(xiàn)有倆setValue,讓人困惑。
重新實現(xiàn)Child2,繼承Parent時String作為泛型T類型,并使用@Override注解setValue,實現(xiàn)有效的方法重寫
但還是出現(xiàn)重復(fù)日志
Child2的setValue調(diào)了兩次。難道是JDK的反射出Bug了!
通過getDeclaredMethods查找到的方法肯定來自Child2本身;而且Child2類中看起來也只有一個setValue,怎么可能還重復(fù)?
調(diào)試發(fā)現(xiàn),Child2類其實有倆setValue:入?yún)⒎謩e是String/Object。
這就是泛型類型擦除導(dǎo)致。
解密反射下的泛型擦除大坑
Java泛型類型在編譯后被擦除為Object。子類雖指定父類泛型T類型是String,但編譯后T會被擦除成為Object,所以父類setValue入?yún)⑹荗bject,value也是Object。
若Child2 setValue想覆蓋父類,那入?yún)⒁岔殲镺bject。所以,編譯器會為我們生成一個橋接方法
Child2類的class字節(jié)碼:
Compiled from "GenericAndInheritanceApplication.java" class Child2 extends Parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
若編譯器未幫我們實現(xiàn)該橋接方法,那Child2重寫的是父類泛型類型擦除后、入?yún)⑹荗bject的setValue。這兩個方法的參數(shù),一個String一個Object,明顯不符合Java語義:
class Parent { AtomicInteger updateCount = new AtomicInteger(); private Object value; public void setValue(Object value) { System.out.println("Parent.setValue called"); this.value = value; updateCount.incrementAndGet(); } } class Child2 extends Parent { @Override public void setValue(String value) { System.out.println("Child2.setValue called"); super.setValue(value); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用jclasslib打開Child2,可看到入?yún)镺bject的橋接方法上標(biāo)記public synthetic bridge。synthetic代表由編譯器生成的不可見代碼,bridge代表這是泛型類型擦除后生成的橋接代碼
修改
使用method的isBridge方法,來判斷方法是不是橋接方法:
通過getDeclaredMethods方法獲取到所有方法后,必須同時根據(jù)方法名setValue和非isBridge兩個條件過濾,才能實現(xiàn)唯一過濾
使用Stream時,如果希望只匹配0或1項的話,可以考慮配合ifPresent來使用findFirst方法。
Arrays.stream(child2.getClass().getDeclaredMethods()) .filter(method -> method.getName().equals("setValue") && !method.isBridge()) .findFirst().ifPresent(method -> { try { method.invoke(chi2, "test"); } catch (Exception e) { e.printStackTrace(); } });
1
2
3
4
5
6
7
8
9
使用反射查詢類方法清單時:
getMethods和getDeclaredMethods是有區(qū)別的,前者可以查詢到父類方法,后者只能查詢到當(dāng)前類
反射進行方法調(diào)用要注意過濾橋接方法。
參考
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html
https://docs.oracle.com/javase/tutorial/reflect/index.html
https://docs.oracle.com/javase/8/docs/technotes/guides/language/annotations.html
https://docs.oracle.com/javase/tutorial/java/annotations/index.html
https://docs.oracle.com/javase/8/docs/technotes/guides/language/generics.html
https://docs.oracle.com/javase/tutorial/java/generics/index.html
Java
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。