JavaRush /Java Blog /Random-TW /Java 中的反射 - 使用範例

Java 中的反射 - 使用範例

在 Random-TW 群組發布
您可能在日常生活中遇到過「反思」的概念。通常這個詞指的是研究自己的過程。在程式設計中,它具有類似的含義 - 它是一種檢查程式資料以及在程式執行期間​​更改程式的結構和行為的機制。這裡重要的是它是在運行時完成的,而不是在編譯時完成的。但為什麼要在運行時檢查程式碼呢?你已經看到了:/使用反射的範例 - 1反思的想法可能不會立即清晰,原因有一個:直到這一刻,你一直知道你正在使用的類別。好吧,例如,您可以編寫一個類別Cat
package learn.javarush;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
你知道它的一切,你知道它有哪些欄位和方法。Animal當然,如果程式突然需要其他類別的動物,為了方便起見,您可以建立一個具有公共類別的繼承系統。以前,我們甚至創建了一個獸醫診所類,您可以在其中傳遞父對象Animal,並且程式將根據動物是狗還是貓來對待它。儘管這些任務不是很簡單,但程式會在編譯時學習所需的所有關於類別的資訊。因此,當您將方法中的main()物件傳遞Cat給獸醫診所類別的方法時,程式已經知道這是一隻貓,而不是狗。現在讓我們想像一下我們面臨著另一項任務。我們的目標是編寫一個程式碼分析器。我們需要創建一個CodeAnalyzer具有單一方法的類別 - void analyzeClass(Object o)。該方法應該:
  • 確定物件傳遞給它的類別並在控制台中顯示類別名稱;
  • 確定該類別的所有字段的名稱,包括私有字段,並將其顯示在控制台中;
  • 確定該類別的所有方法的名稱,包括私有方法,並將它們顯示在控制台中。
它看起來像這樣:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
現在這個問題和你之前解決的其他問題之間的差異是顯而易見的。在這種情況下,困難在於您和程式都不知道到底將傳遞給該方法的內容是什麼analyzeClass()。您編寫一個程序,其他程式設計師將開始使用它,他們可以將任何內容傳遞到此方法中 - 任何標準 Java 類別或他們編寫的任何類別。該類別可以有任意數量的變數和方法。換句話說,在這種情況下,我們(和我們的程式)不知道我們將使用哪些類別。然而,我們必須解決這個問題。標準 Java 函式庫可以為我們提供協助-Java Reflection API。Reflection API 是一項強大的語言功能。Oracle 官方文件指出,建議僅由非常了解自己在做什麼的經驗豐富的程式設計師使用此機制。您很快就會明白為什麼我們會突然提前收到此類警告:) 以下是使用 Reflection API 可以完成的操作的清單:
  1. 找出/確定物件的類別。
  2. 取得有關類別修飾符、欄位、方法、常數、建構函式和超類別的資訊。
  3. 找出哪些方法屬於已實現的介面。
  4. 當程式執行之前類別名稱未知時,建立類別的實例。
  5. 按名稱取得和設定物件欄位的值。
  6. 按名稱呼叫物件的方法。
令人印象深刻的清單,對吧?:) 注意:無論我們將哪個類別物件傳遞給程式碼分析器,反射機制都能夠「即時」執行所有這些操作!讓我們透過範例來了解 Reflection API 的功能。

如何找出/確定物件的類別

讓我們從基礎開始。Java 反射機制的入口點是Class. 是的,它看起來真的很有趣,但這就是反射的用途:)使用類Class,我們首先確定傳遞給我們方法的任何物件的類別。讓我們試試這個:
import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
控制台輸出:

class learn.javarush.Cat
注意兩件事。首先,我們故意將類別放在Cat一個單獨的套件中,learn.javarush;現在你可以看到它getClass()傳回了類別的全名。其次,我們將變數命名為clazz。看起來有點奇怪。當然應該叫“class”,但是“class”是Java語言中的保留字,編譯器不會允許這樣呼叫變數。我必須擺脫它:) 好吧,這是一個不錯的開始!我們還有什麼可能性?

如何取得有關類別修飾符、欄位、方法、常數、建構函式和超類別的信息

這已經更有趣了!在目前類別中,我們沒有常數,也沒有父類別。為了完整起見,我們添加它們。讓我們建立最簡單的父類別Animal
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
讓我們在我們的類別中加入一個Cat繼承Animal和一個常數:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
現在我們已經有一整套了!讓我們試試看反思的可能性:)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
這是我們在控制台中得到的結果:
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
我們收到了有關課程的非常詳細的資訊!不僅涉及公共部分,還涉及私人部分。 注意: private- 變數也顯示在清單中。實際上,到目前為止,類別的「分析」就可以被認為完成了:現在,使用該方法,analyzeClass()我們將學習所有可能的內容。但這些並不是我們在進行反思時所擁有的全部可能性。讓我們不要局限於簡單的觀察,而要積極行動!:)

如果在程式執行之前類別名稱未知,如何建立類別的實例

讓我們從預設建構函數開始。它不在我們的類別中Cat,所以讓我們添加它:
public Cat() {

}
以下是使用反射創建物件的程式碼Cat(方法createCat()):
import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
進入控制台:

learn.javarush.Cat
控制台輸出:

Cat{name='null', age=0}
這不是錯誤:值nameage顯示在控制台中,因為我們在toString()類別方法中編寫了它們的輸出Cat。在這裡,我們讀取將從控制台建立其物件的類別的名稱。正在運行的程式了解它將創建其物件的類別的名稱。 使用反射的範例 - 3為了簡潔起見,我們省略了正確的異常處理程式碼,這樣它就不會比範例本身佔用更多的空間。當然,在真實的程式中,絕對值得處理輸入錯誤名稱等情況。預設建構函數是一個相當簡單的事情,因此如您所見,使用它創建類別的實例並不困難:)並且使用該方法,newInstance()我們創建了此類的一個新物件。Cat如果類別構造函數將參數作為輸入,則是另一回事。讓我們從類別中刪除預設建構函數並嘗試再次運行我們的程式碼。

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
出了點問題!我們收到錯誤,因為我們呼叫了透過預設建構函式創建物件的方法。但現在我們沒有這樣的設計師。這意味著當該方法起作用時,newInstance()反射機制將使用帶有兩個參數的舊建構函數:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
但我們沒有對這些參數做任何事情,就好像我們完全忘記了它們一樣!要使用反射將它們傳遞給建構函數,您必須稍微調整一下:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
控制台輸出:

Cat{name='Barsik', age=6}
讓我們仔細看看我們的程式中發生了什麼。我們創建了一個物件數組Class
Class[] catClassParams = {String.class, int.class};
它們對應到我們建構函數的參數(我們只有參數Stringint)。我們將它們傳遞給方法clazz.getConstructor()並存取所需的建構函數。之後,剩下的就是使用newInstance()必要的參數呼叫該方法,並且不要忘記將物件明確轉換為我們需要的類別 - Cat
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
這樣,我們的物件就創建成功了!控制台輸出:

Cat{name='Barsik', age=6}
讓我們繼續 :)

如何透過名稱取得和設定物件欄位的值

想像一下您正在使用另一個程式設計師編寫的類別。但是,您沒有機會對其進行編輯。例如,打包在 JAR 中的現成的類別庫。您可以閱讀類別程式碼,但無法更改它。在這個庫中創建類別的程式設計師(讓它成為我們的舊類別Cat)在最終設計之前沒有得到足夠的睡眠,並刪除了該字段的 getter 和 setter age。現在這堂課已經來到你身邊了。它完全滿足您的需求,因為您只需要程式中的物件Cat。但你需要他們在同一領域age!這是一個問題:我們無法到達該字段,因為它有一個修飾符private,並且 getter 和 setter 被此類的潛在開發人員刪除了:/好吧,反射在這種情況下也可以幫助我們!Cat我們可以存取類別代碼:我們至少可以找出它有哪些欄位以及它們的名稱。有了這些訊息,我們就可以解決我們的問題:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
正如評論中所述,name該欄位的一切都很簡單:類別開發人員為其提供了一個設定器。您還已經知道如何從預設建構函數建立物件:有一個 this 方法newInstance()。但你必須修改第二個字段。讓我們弄清楚這裡發生了什麼:)
Field age = clazz.getDeclaredField("age");
在這裡,我們使用我們的對象Class clazz,使用 存取age該欄位getDeclaredField()。它使我們能夠將年齡字段作為對象獲取Field age。但這還不夠,因為private欄位不能簡單地賦值。為此,您需要使用以下方法使該欄位「可用」setAccessible()
age.setAccessible(true);
可以為完成此操作的那些欄位指派值:
age.set(cat, 6);
正如您所看到的,我們有一個顛倒的設定器:我們為欄位分配Field age其值,並將該欄位應分配到的物件傳遞給它。讓我們運行我們的方法main()來看看:

Cat{name='Barsik', age=6}
太棒了,我們都做到了!:) 讓我們看看還有什麼其他的可能性...

如何透過名稱呼叫物件的方法

讓我們稍微改變一下前面例子的情況。假設類別開發人員Cat在字段上犯了一個錯誤 - 兩者都可用,有它們的 getter 和 setter,一切都很好。問題是不同的:他將我們肯定需要的方法設為私有:
private void sayMeow() {

   System.out.println("Meow!");
}
結果,我們將Cat在程式中建立對象,但無法呼叫它們的方法sayMeow()。我們會有不會喵喵叫的貓嗎?很奇怪:/我該如何解決這個問題?Reflection API 再次拯救您!我們知道所需方法的名稱。剩下的就是技術問題了:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Barsik", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
在這裡,我們的行為方式與存取私有欄位的情況大致相同。首先我們得到我們需要的方法,它被封裝在一個類別物件中Method
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
在幫助下,getDeclaredMethod()您可以「接觸」私有方法。接下來我們讓該方法可呼叫:
sayMeow.setAccessible(true);
最後,我們呼叫所需物件的方法:
sayMeow.invoke(cat);
呼叫方法也看起來像「反向呼叫」:我們習慣使用點 ( cat.sayMeow()) 將物件指向所需的方法,而在使用反射時,我們將需要呼叫該方法的物件傳遞給該方法。我們在控制台中有什麼?

Meow!
一切順利!:) 現在您可以看到 Java 中的反射機制為我們提供了多麼廣泛的可能性。在困難和意外的情況下(例如在封閉庫中的類別的範例中),它確實可以幫助我們很多。然而,就像任何大國一樣,它也意味著巨大的責任。Oracle 網站上有專門的章節介紹了反射的缺點。主要有以下三個缺點:
  1. 生產力下降。使用反射調用的方法的性能低於通常調用的方法。

  2. 有安全限制。反射機制可讓您在執行時間更改程式的行為。但在實際專案的工作環境中,可能會有一些限制,不允許您執行此操作。

  3. 內線資訊外洩的風險。重要的是要理解,使用反射直接違反了封裝原則:它允許我們存取私有欄位、方法等。我認為沒有必要解釋,只有在最極端的情況下,當由於你無法控制的原因而沒有其他方法來解決問題時,才應該訴諸直接和粗暴地違反 OOP 原則。

明智地使用反射機制,並且僅在無法避免的情況下使用,並且不要忘記它的缺點。我們的講座到此結束!結果相當大,但今天你學到了很多新東西:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION