拼夕夕三輪面經:被問到反射的bug,你中招了嗎?

看到題目,不少初級開發同窗就慌了,我每天 crud,什麼時候用過反射呀?並且它竟然還有 bug 的嗎?html

其實否則,反射在咱們平常開發中一直陪伴着咱們,若是咱們一點不重視反射的使用,就會不自覺地寫出不少沒法理解的 bug。今天咱們就來看看增刪改查是如何遇到反射bug的!java

1 當反射碰見重載

重載level方法,入參分別是intInteger若不使用反射,選用哪一個重載方法很清晰,好比:git

  • 傳入666就走int參數重載
  • 傳入Integer.valueOf(「666」)走Integer重載

那反射調用方法也是根據入參類型肯定使用哪一個重載方法嗎? 使用getDeclaredMethod獲取 grade方法,而後傳入Integer.valueOf(「36」) 結果是: 由於反射進行方法調用是經過bash

方法簽名

來肯定方法。本例的getDeclaredMethod傳入的參數類型Integer.TYPE其實表明int 因此無論傳包裝類型仍是基本類型,最終都是調用int入參重載方法。markdown

Integer.TYPE改成Integer.class,則實際執行的參數類型就是Integer了。且不管傳包裝類型仍是基本類型,最終都調用Integer入參重載方法。oracle

綜上,反射調用方法,是以反射獲取方法時傳入的方法名和參數類型來肯定調用的方法。ide

2 泛型的類型擦除

泛型容許SE使用類型參數替代精確類型,實例化時再指明具體類型。利於代碼複用,將一套代碼應用到多種數據類型。oop

泛型的類型檢測,能夠在編譯時檢查不少泛型編碼錯誤。但因爲歷史兼容性而妥協的泛型類型擦除方案,在運行時還有不少坑。ui

案例

如今指望在類的字段內容變更時記錄日誌,因而SE想到定義一個泛型父類,並在父類中定義一個統一的日誌記錄方法,子類可繼承該方法。上線後總有日誌重複記錄。編碼

  • 父類

  • 子類1

  • 經過反射調用子類方法:

雖Base.value正確設置爲了JavaEdge,但父類setValue調用了兩次,計數器顯示2

兩次調用Base.setValue,是由於getMethods找到了兩個setValue

子類重寫父類方法失敗緣由

  • 子類未指定String泛型參數,父類的泛型方法setValue(T value)泛型擦除後是setValue(Object value),因而子類入參String的setValue被看成新方法
  • 子類的setValue未加@Override註解,編譯器未能檢測到重寫失敗

有的同窗會認爲是由於反射API使用錯誤致使而非重寫失敗:

  • getMethods

獲得當前類和父類的全部public方法

  • getDeclaredMethods

得到當前類全部的public、protected、package和private方法

因而用getDeclaredMethods替換getMethods 雖然這樣作能夠規避重複記錄日誌,但未解決子類重寫父類方法失敗的問題

  • 使用Sub1時仍是會發現有倆個setValue

因而,終於明白還得從新實現Sub2,繼承Base時將String做爲泛型T類型,並使用 @Override 註解 setValue

  • 但仍是出現重複日誌

Sub2的setValue居然調用了兩次,難道是JDK反射有Bug!getDeclaredMethods查找到的方法確定來自Sub2;並且Sub2看起來也就一個setValue,怎麼會重複?

調試發現,Child2類其實有倆setValue:入參分別是String、Object。 這就是由於泛型類型擦除

反射下的泛型擦除「天坑」

Java泛型類型在編譯後被擦除爲Object。子類雖指定父類泛型T類型是String,但編譯後T會被擦除成爲Object,因此父類setValue入參是Object,value也是Object。 若Sub2.setValue想重寫父類,那入參也須爲Object。因此,編譯器會爲咱們生成一個橋接方法。 Sub2類的class字節碼:

➜  genericandinheritance git:(master) ✗ javap -c Sub2.class
Compiled from "GenericAndInheritanceApplication.java"
class com.javaedge.oop.genericandinheritance.Sub2 extends com.javaedge.oop.genericandinheritance.Base<java.lang.String> {
  com.javaedge.oop.genericandinheritance.Sub2();
    Code:
       0: aload_0
       1: invokespecial #1 // Method com/javaedge/oop/genericandinheritance/Base."<init>":()V
       4: return

  public void setValue(java.lang.String);
    Code:
       0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3 // String call Sub2.setValue
       5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: aload_0
       9: aload_1
      10: invokespecial #5 // Method com/javaedge/oop/genericandinheritance/Base.setValue:(Ljava/lang/Object;)V
      13: return

  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #6 // class java/lang/String
       // 入參爲Object的setValue在內部調用了入參爲String的setValue方法
       5: invokevirtual #7 // Method setValue:(Ljava/lang/String;)V
       8: return
}
複製代碼

若編譯器未幫咱們實現該橋接方法,則Sub2重寫的是父類泛型類型擦除後、入參是Object的setValue。這兩個方法的參數,一個String一個Object,顯然不符Java重寫。

入參爲Object的橋接方法上標記了public synthetic bridge

  • synthetic表明由編譯器生成的不可見代碼
  • bridge表明這是泛型類型擦除後生成的橋接代碼

修正

知道了橋接方法的存在,如今就該知道如何修正代碼了。

  • 經過getDeclaredMethods獲取全部方法後,還得加上非isBridge這個過濾條件:

  • 結果

參考

相關文章
相關標籤/搜索