你好,年輕的 Padawan。在本文中,我將向您介紹原力,這是 Java 程序員僅在看似不可能的情況下才會使用的一種力量。Java 的陰暗面是反射 API。在 Java 中,反射是使用 Java Reflection API 實現的。
在我的包層次結構中, MyClass的全名是“reflection.MyClass”。還有一種學習類名的簡單方法(將類名作為字符串返回):
什麼是Java反射?
互聯網上有一個簡短、準確且流行的定義。反射(來自晚期拉丁語 reflexio - 回頭)是一種在程序運行時探索有關數據的機制。反射讓您探索有關字段、方法和類構造函數的信息。反射使您可以使用在編譯時不存在但在運行時可用的類型。反射和用於發布錯誤信息的邏輯一致模型使得創建正確的動態代碼成為可能。換句話說,了解反射在 Java 中的工作原理將為您打開許多驚人的機會。您實際上可以兼顧類及其組件。這是反射允許的基本列表:- 學習/確定對象的類別;
- 獲取有關類的修飾符、字段、方法、常量、構造函數和超類的信息;
- 找出哪些方法屬於已實現的接口;
- 創建一個直到運行時類名未知的類的實例;
- 按名稱獲取和設置對象字段的值;
- 按名稱調用對象的方法。
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);
}
}
如您所見,這是一個非常基礎的類。帶參數的構造函數被特意註釋掉了。我們稍後再談。如果您仔細查看類的內容,您可能會注意到name字段缺少getter。名稱字段本身標有私有訪問修飾符:我們無法在類本身之外訪問它,這意味著我們無法檢索它的值。“那有什麼問題嗎?” 你說。“添加getter或更改訪問修飾符”。你是對的,除非MyClass
位於已編譯的 AAR 庫或另一個無法進行更改的私有模塊中。實際上,這種情況經常發生。而一些粗心的程序員根本就忘了寫一個getter。這是記住反省的好時機!讓我們嘗試進入類的私有名稱字段MyClass
:
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()
方法。此方法還返回一個類字段數組,但現在它包括私有字段和受保護字段。在這種情況下,我們知道我們感興趣的字段的名稱,因此我們可以使用該getDeclaredField(String)
方法,其中String
是所需字段的名稱。 筆記: getFields()
並且getDeclaredFields()
不要返回父類的字段!偉大的。我們得到了一個引用我們名字Field
的對象。由於該字段不是public,我們必須授予訪問權限才能使用它。該方法讓我們進一步進行。現在名稱字段在我們的完全控制之下!您可以通過調用對象的方法來檢索它的值,其中是我們類的一個實例。我們將類型轉換為並將值分配給我們的名稱變量。如果我們找不到一個setter來給 name 字段設置一個新的值,可以使用set方法: setAccessible(true)
Field
get(Object)
Object
MyClass
String
field.set(myClass, (String) "new value");
恭喜!您剛剛掌握了反射的基礎知識並訪問了私有字段! 注意塊try/catch
,以及正在處理的異常類型。IDE 會告訴您它們本身是必需的,但您可以通過它們的名稱清楚地告訴您它們為什麼會出現在這裡。繼續!您可能已經註意到,我們的MyClass
類已經有一個方法來顯示有關類數據的信息:
private void printData(){
System.out.println(number + name);
}
但是這個程序員也在這裡留下了他的指紋。方法有私有訪問修飾符,每次顯示數據都得自己寫代碼。真是一團糟。我們的反思去哪兒了?編寫以下函數:
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(Object, Args)
方法,其中Object
也是類的一個實例MyClass
。Args
是方法的參數,儘管我們的沒有。現在我們使用printData
函數來顯示信息:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // Output: 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
}
歡呼!現在我們可以訪問該類的私有方法。但是,如果該方法確實有參數怎麼辦,為什麼構造函數被註釋掉了呢?一切都在適當的時候。從一開始的定義就清楚了,反射讓你可以在運行時(程序運行時)創建一個類的實例!我們可以使用類的全名創建一個對象。類的全稱是類名,包括其包的路徑。

MyClass.class.getName()
讓我們使用 Java 反射來創建類的實例:
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
,那麼ClassLoader
負責將類加載到 JVM 中的 將永遠不會加載該類。這意味著您必須強制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 技術離不開 Java Reflection API。例如依賴注入(Dependency Injection,DI),將註解與方法和構造函數的反射相結合,形成了流行的DarerAndroid 開發庫。閱讀本文後,您可以自信地認為自己已掌握 Java 反射 API 的方法。他們不會無緣無故地將反射稱為 Java 的陰暗面。它完全打破了 OOP 範式。在 Java 中,封裝隱藏並限制了其他人對某些程序組件的訪問。當我們使用 private 修飾符時,我們希望該字段只能從它所在的類中訪問。並且我們基於這個原則構建程序的後續架構。在本文中,我們了解瞭如何使用反射來強制執行任何操作。 創建型設計模式Singleton是一個很好的例子,作為一個架構解決方案。基本思想是實現此模式的類在整個程序執行期間將只有一個實例。這是通過將私有訪問修飾符添加到默認構造函數來實現的。如果程序員使用反射來創建此類類的更多實例,那將是非常糟糕的。對了,最近聽同事問了一個很有意思的問題:實現了單例模式的類可以繼承嗎?難道,在這種情況下,連反思都無能為力了?在下面的評論中留下您對本文的反饋和答案,並在那裡提出您自己的問題!
更多閱讀: |
---|
GO TO FULL VERSION