CodeGym/Java Blog/Toto sisi/反思的例子
John Squirrels
等級 41
San Francisco

反思的例子

在 Toto sisi 群組發布
個成員
也許你在日常生活中遇到過“反射”的概念。這個詞通常指的是學習自己的過程。在編程中,它具有類似的含義——它是一種機制,用於分析有關程序的數據,甚至可以在程序運行時更改程序的結構和行為。 反思的例子 - 1 這裡重要的是我們在運行時而不是編譯時執行此操作。但是為什麼要在運行時檢查代碼呢?畢竟,您已經可以閱讀代碼了:/ 反射的概念可能不會立即清晰,這是有原因的:到目前為止,您始終知道自己在使用哪些類。例如,您可以編寫一個Cat類:
package learn.codegym;

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對象(父類的實例)傳遞給它,程序根據動物是狗還是貓來適當地對待它。儘管這些不是最簡單的任務,但程序能夠在編譯時了解有關類的所有必要信息。因此,當您將對Cat像傳遞給main()方法,程序已經知道它是一隻貓,而不是一隻狗。現在讓我們假設我們面臨著不同的任務。我們的目標是編寫一個代碼分析器。我們需要CodeAnalyzer用一個方法創建一個類:void analyzeObject(Object o)。這個方法應該:
  • 確定傳遞給它的對象的類,並在控制台上顯示類名;
  • 確定傳遞類的所有字段的名稱,包括私有字段,並將它們顯示在控制台上;
  • 確定傳遞類的所有方法的名稱,包括私有方法,並將它們顯示在控制台上。
它看起來像這樣:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }

}
現在我們可以清楚地看到這個任務與您之前解決的其他任務有何不同。對於我們當前的目標,困難在於我們和程序都不知道將傳遞給analyzeClass()方法。如果您編寫這樣的程序,其他程序員將開始使用它,並且他們可能將任何東西傳遞給此方法——任何標準 Java 類或他們編寫的任何其他類。傳遞的類可以有任意數量的變量和方法。換句話說,我們(和我們的程序)不知道我們將使用哪些類。但是,我們仍然需要完成這項任務。這就是標準 Java Reflection API 為我們提供幫助的地方。Reflection API 是該語言的一個強大工具。Oracle 的官方文檔建議這種機制應該只由有經驗的程序員使用,他們知道自己在做什麼。您很快就會明白為什麼我們要提前發出此類警告 :) 以下是您可以使用 Reflection API 執行的操作的列表:
  1. 識別/確定對象的類別。
  2. 獲取有關類修飾符、字段、方法、常量、構造函數和超類的信息。
  3. 找出哪些方法屬於已實現的接口。
  4. 創建一個類的實例,其類名在程序執行之前是未知的。
  5. 按名稱獲取和設置實例字段的值。
  6. 按名稱調用實例方法。
令人印象深刻的名單,是吧?:) 筆記:反射機制可以“即時”完成所有這些事情,無論我們傳遞給代碼分析器的對像類型如何!讓我們通過一些示例來探索反射 API 的功能。

如何識別/確定對象的類別

讓我們從基礎開始。Java 反射引擎的入口點是類Class。是的,它看起來很有趣,但這就是反射 :) 使用類Class,我們首先確定傳遞給我們方法的任何對象的類。讓我們嘗試這樣做:
import learn.codegym.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("Fluffy", 6));
   }
}
控制台輸出:
class learn.codegym.Cat
注意兩件事。首先,我們特意將Cat類放在一個單獨的learn.codegym包中。現在您可以看到該getClass()方法返回類的全名。其次,我們將變量命名為clazz。這看起來有點奇怪。稱它為“類”是有意義的,但“類”是 Java 中的保留字。編譯器不允許這樣調用變量。我們必須以某種方式解決這個問題 :) 一開始還不錯!我們在該功能列表中還有什麼?

如何獲取有關類修飾符、字段、方法、常量、構造函數和超類的信息。

現在事情變得越來越有趣了!在當前類中,我們沒有任何常量或父類。讓我們添加它們以創建完整的圖片。創建最簡單的Animal父類:
package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
我們將使我們的Cat類繼承Animal並添加一個常量:
package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
現在我們有了完整的畫面!讓我們看看反射的能力:)
import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
這是我們在控制台上看到的:
Class name:  class learn.codegym.Cat
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Parent class: class learn.codegym.Animal
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
看看我們能夠獲得的所有詳細的課程信息!不僅是公共信息,還有私人信息! 筆記: private變量也顯示在列表中。我們對課程的“分析”可以認為基本上是完整的:我們正在使用該analyzeObject()方法來學習我們能學到的一切。但這並不是我們可以用反射做的一切。我們不局限於簡單的觀察——我們將繼續採取行動!:)

如何創建一個類的實例,其類名在程序執行之前是未知的。

讓我們從默認構造函數開始。我們Cat班還沒有,所以讓我們添加它:
public Cat() {

}
Cat下面是使用反射(createCat()方法) 創建對象的代碼:
import learn.codegym.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.codegym.Cat
控制台輸出:
Cat{name='null', age=0}
name這不是錯誤:和 的值age顯示在控制台上,因為我們編寫了代碼將它們輸出到類toString()的方法中Cat。在這裡,我們讀取了一個類的名稱,我們將從控制台創建其對象。該程序識別要創建其對象的類的名稱。 反思的例子 - 3為了簡潔起見,我們省略了適當的異常處理代碼,這將佔用比示例本身更多的空間。在實際程序中,當然,您應該處理涉及輸入錯誤名稱等情況。默認構造函數非常簡單,所以如您所見,使用它很容易創建類的實例:) 使用newInstance()方法,我們創建了這個類的一個新對象。這是另一回事,如果Cat構造函數將參數作為輸入。讓我們刪除該類的默認構造函數並嘗試再次運行我們的代碼。
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
出了些問題!我們得到了一個錯誤,因為我們調用了一個使用默認構造函數創建對象的方法。但是我們現在沒有這樣的構造函數。所以當newInstance()方法運行時,反射機制使用我們帶有兩個參數的舊構造函數:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
但是我們沒有對參數做任何事情,就好像我們完全忘記了它們一樣!使用反射將參數傳遞給構造函數需要一點“創造力”:
import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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='Fluffy', age=6}
讓我們仔細看看我們的程序中發生了什麼。我們創建了一個Class對像數組。
Class[] catClassParams = {String.class, int.class};
它們對應於我們構造函數的參數(只有Stringint參數)。我們將它們傳遞給clazz.getConstructor()方法並獲得對所需構造函數的訪問權限。之後,我們需要做的就是newInstance()使用必要的參數調用該方法,並且不要忘記將對象顯式轉換為所需的類型:Cat
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
現在我們的對象創建成功了!控制台輸出:
Cat{name='Fluffy', age=6}
向右移動:)

如何通過名稱獲取和設置實例字段的值。

想像一下,您正在使用另一個程序員編寫的類。此外,您無權對其進行編輯。比如打包成JAR的現成類庫。您可以閱讀類的代碼,但不能更改它。假設在這個庫中創建其中一個類(讓它成為我們的舊Cat類)的程序員,在設計定稿的前一晚沒有睡夠,刪除了該age字段的 getter 和 setter。現在這堂課已經來到你身邊。它滿足您的所有需求,因為您只需要Cat程序中的對象。但是你需要他們有一個age領域!這是一個問題:我們無法到達該字段,因為它有private修飾符,getter 和 setter 被創建類的睡眠不足的開發人員刪除了:/ 好吧,反射可以在這種情況下幫助我們!我們可以訪問該類的代碼Cat,因此我們至少可以找出它有哪些字段以及它們的名稱。有了這些信息,我們就可以解決我們的問題:
import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           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字段的所有內容都很簡單,因為類開發人員提供了一個設置器。您已經知道如何從默認構造函數創建對象:我們有newInstance()為此。但是我們必須對第二個字段進行一些修補。讓我們弄清楚這裡發生了什麼:)
Field age = clazz.getDeclaredField("age");
在這裡,使用我們的Class clazz對象,我們age通過getDeclaredField()方法訪問該字段。它讓我們將年齡字段作為Field age對象。但這還不夠,因為我們不能簡單地為private字段賦值。為此,我們需要使用以下setAccessible()方法使該字段可訪問:
age.setAccessible(true);
一旦我們對一個字段執行此操作,我們就可以分配一個值:
age.set(cat, 6);
如您所見,我們的Field age對像有一種由內而外的 setter,我們向其傳遞一個 int 值和要分配其字段的對象。我們運行我們的main()方法並看到:
Cat{name='Fluffy', age=6}
出色的!我們做到了!:) 讓我們看看我們還能做些什麼......

如何通過名稱調用實例方法。

讓我們稍微改變一下前面示例中的情況。假設Cat類開發人員沒有在 getter 和 setter 上出錯。在這方面一切都很好。現在問題不同了:有一個我們確實需要的方法,但開發人員將其設為私有:
private void sayMeow() {

   System.out.println("Meow!");
}
這意味著如果我們Cat在我們的程序中創建對象,那麼我們將無法調用sayMeow()它們的方法。我們會有不會喵喵叫的貓嗎?這很奇怪:/我們將如何解決這個問題?反射 API 再一次幫助了我們!我們知道我們需要的方法的名稱。其他一切都是技術問題:
import learn.codegym.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("Fluffy", 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 原則的行為僅在最極端的情況下,即由於您無法控制的原因而沒有其他方法可以解決問題時。

明智地使用反射,並且只在無法避免的情況下使用,並且不要忘記它的缺點。至此,我們的課程就告一段落了。結果很長,但你今天學到了很多東西:)
留言
  • 受歡迎
你必須登入才能留言
此頁面尚無留言