JavaRush /Java Blog /Random-TW /反射 API。反射。Java 的黑暗面
Oleksandr Klymenko
等級 13
Харків

反射 API。反射。Java 的黑暗面

在 Random-TW 群組發布
你好,年輕的學徒。在這篇文章中,我將向您介紹原力,Java 程式設計師僅在看似絕望的情況下使用它的力量。所以,Java 的陰暗面是 -Reflection API
反射 API。 反射。 Java 的黑暗面 - 1
Java 中的反射是使用 Java Reflection API 完成的。這個反射是什麼?有一個簡短而精確的定義在網路上也很流行。 反射 (來自後期拉丁語 reflexio - 回溯)是一種研究程序執行期間數據的機制。反射可讓您檢查有關欄位、方法和類別建構函式的資訊。反射機製本身可讓您處理編譯期間不存在但在程式執行期間​​出現的類型。反射和用於報告錯誤的邏輯一致模型的存在使得創建正確的動態程式碼成為可能。換句話說,了解反射在 Java 中的工作原理將為您帶來許多驚人的機會。您實際上可以同時使用類別及其元件。
反射 API。 反射。 Java 的黑暗面 - 2
以下是反射允許的基本清單:
  • 找出/確定物件的類別;
  • 取得有關類別修飾符、欄位、方法、常數、建構函式和超類別的資訊;
  • 找出哪些方法屬於已實現的介面;
  • 創建類別的實例,類別的名稱在程式執行之前是未知的;
  • 透過名稱取得和設定物件欄位的值;
  • 按名稱呼叫物件的方法。
幾乎所有現代 Java 技術都使用了反射。很難想像 Java 作為一個平台是否能夠在不經過反思的情況下獲得如此廣泛的採用。我很可能不能。你已經熟悉了反射的一般理論思想,現在讓我們開始實際應用吧!我們不會研究Reflection API的所有方法,只研究實務上實際遇到的。由於反射機制涉及使用類,因此我們將有一個簡單的類 - MyClass
public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
正如我們所看到的,這是最常見的類別。帶參數的建構子被註解掉是有原因的,我們稍後會再討論這一點。如果您仔細查看課程內容,您可能會發現getter'a的缺失name。該欄位本身name標有存取修飾符private;我們無法在類別本身之外存取它;=>我們無法取得它的值。「所以有什麼問題?- 你說。“新增getter或變更存取修飾符。” 你是對的,但如果MyClass它位於已編譯的 aar 函式庫或另一個沒有編輯存取權限的封閉模組中,該怎麼辦,而實際上這種情況經常發生。而一些不專心的程式設計師乾脆忘了寫getter。是時候記住反思了!讓我們嘗試進入類別private字段: nameMyClass
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
現在讓我們弄清楚這裡發生了什麼。java中有一個很棒的課程Class。它表示可執行 Java 應用程式中的類別和介面。Class我們不會觸及和之間的聯繫ClassLoader。這不是本文的主題。接下來,要取得該類別的字段,需要呼叫該方法getFields(),該方法將傳回該類別的所有可用字段。這不適合我們,因為我們的字段是private,所以我們使用方法getDeclaredFields()。這個方法也返回一個類別字段數組,但是現在privateprotected。在我們的情況下,我們知道我們感興趣的欄位的名稱,我們可以使用方法getDeclaredField(String),其中String是所需欄位的名稱。 筆記: getFields()並且getDeclaredFields()不傳回父類別的欄位!太棒了,我們收到了一個Field 帶有我們的連結的物件name。因為 該領域不是публичным(公共)的,因此應給予其使用的權限。該方法setAccessible(true)使我們能夠繼續工作。現在戰場name已經完全在我們的掌控之中了!get(Object)您可以透過呼叫該物件來取得它的值Field,其中Object是我們類別的一個實例MyClass。我們將其轉換String並將其分配給我們的變數name。如果我們突然沒有setter'a,我們可以使用該方法為 name 欄位設定一個新值set
field.set(myClass, (String) "new value");
恭喜!您剛剛掌握了反射的基本機制,並且能夠進入該private領域了! 注意try/catch處理的異常區塊和類型。IDE 本身會指示它們的強制存在,但它們的名稱清楚地表明了它們存在的原因。前進!正如您可能已經注意到的,我們MyClass已經有一個用於顯示有關類別資料的資訊的方法:
private void printData(){
       System.out.println(number + name);
   }
但這位程式設計師也在這裡留下了遺產。這個方法位於存取修飾符下private,每次我們都必須自己編寫輸出程式碼。不按順序,我們的反射在哪裡?...我們來寫下面的函數:
public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
這裡的過程與獲取字段大致相同 - 我們通過名稱獲取所需的方法並授予對其的訪問權限。Method並且呼叫我們使用的對象invoke(Оbject, Args),其中Оbject也是類別的實例MyClass Args- 方法參數 - 我們的沒有。現在我們使用該函數來顯示資訊printData
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
萬歲,現在我們可以存取類別的私有方法了。但是如果該方法仍然有參數怎麼辦,為什麼會有一個被註解掉的建構子呢?每個事物都有它的時代。runtime從一開始的定義就可以清楚地看出,反射允許您以某種模式(在程式運行時)創建類別的實例!我們可以透過該類別的完全限定名稱來建立該類別的物件。完全限定類別名稱是類別的名稱,在package.
反射 API。 反射。 Java 的黑暗面 - 3
在我的層次結構中,package全名MyClass是「reflection.MyClass」。您也可以透過簡單的方式找出類別名稱(它將以字串形式傳回類別名稱):
MyClass.class.getName()
讓我們使用反射來創建該類別的實例:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
當java應用程式啟動時,並不是所有的類別都載入到JVM中。如果你的程式碼沒有引用該類MyClass,那麼負責將類別載入到 JVM 中的人,即ClassLoader永遠不會將其載入到那裡。因此,我們需要強制ClassLoader它載入並接收類型變數形式的類別描述Class。對於此任務,有一個方法forName(String),其中String是我們需要其描述的類別的名稱。收到後Сlass,方法呼叫newInstance()將返回Object,該方法將根據相同的描述建立。剩下的工作就是把這個物件帶到我們的班級MyClass。涼爽的!這很困難,但我希望這是可以理解的。現在我們可以從一行字面上建立類別的實例!不幸的是,所描述的方法僅適用於預設建構函式(不帶參數)。如何呼叫帶參數的方法和帶參數的建構子?是時候取消我們的建構函數的註解了。正如預期的那樣,newInstance()它沒有找到預設建構函數並且不再工作。讓我們重寫類別實例的建立:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
若要取得類別建構函數,請呼叫類別描述中的方法getConstructors(),要取得建構函數參數,請呼叫getParameterTypes()
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
這樣我們就得到了所有的建構子和它們的所有參數。在我的範例中,使用特定的已知參數來呼叫特定的建構子。為了呼叫這個建構函數,我們使用方法newInstance,在該方法中我們指定這些參數的值。invoke呼叫方法也會發生同樣的情況。問題出現了:建構函數的反射呼叫在哪裡有用?如同開頭所提到的,現代 Java 技術離不開 Reflection API。例如,DI(依賴注入),其中註釋與方法和建構函數的反射相結合形成了 Android 開發中流行的 Dagger 庫。讀完本文後,您可以自信地認為自己對 Reflection API 的機制有所了解。反射被稱為java的黑暗面並不是沒有道理的。它完全打破了OOP範式。在java中,封裝用於隱藏和限制某些程式組件對其他程式組件的存取。透過使用 private 修飾符,我們意味著只能在該字段存在的類別中存取該字段,基於此我們建立了程式的進一步架構。在本文中,我們了解如何使用反射來實現目標。架構解決方案形式的一個很好的例子是產生設計模式 - Singleton。它的主要想法是,在程式的整個運行過程中,實作這個模板的類別應該只有一份。這是透過將建構函式的預設存取修飾符設定為 private 來完成的。如果某個程式設計師用自己的反思創建了這樣的類,那將是非常糟糕的。順便說一句,我最近從我的員工那裡聽到一個非常有趣的問題:實作範本的類別可以有Singleton繼承人嗎?難道在這種情況下連反思都無能為力了嗎?在評論中寫下您對文章的回饋和答案,也可以提出您的問題!Reflection API 的真正威力來自於與執行時間註解的結合,我們可能會在以後關於 Java 陰暗面的文章中討論這一點。感謝您的關注!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION