Java基礎(十一)——反射

L發表於2022-02-10

一、概述

1、介紹

  Reflection(反射)是被視為動態語言的關鍵,反射機制允許程式在執行期藉助於Reflection API取得任何類的內部資訊,並能直接操作任意物件的內部屬性及方法。
  載入完類之後,在堆記憶體的方法區中就產生了一個Class型別的物件(一個類只有一個Class物件),這個物件就包含了完整的類的結構資訊。可以通過這個物件看到類的結構。這個物件就像一面鏡子,透過這個鏡子可以看到類的結構,所以,形象的稱之為:反射。

2、動態語言與靜態語言

  動態語言:是一類在執行時可以改變其結構的語言。例如,新的函式、物件,甚至程式碼可以被引進,已有的函式可以被刪除或是其他結構上的變化。通俗點說就是在執行時程式碼可以根據某些條件改變自身結構。主要動態語言:Object-C、C#、JavaScript、PHP、Python、Erlang。
  靜態語言:與動態語言相對應的,執行時結構不可變的語言就是靜態語言。如Java、C、C++。
  Java不是動態語言,但Java可以稱之為"準動態語言"。即Java有一定的動態性,可以利用反射機制、位元組碼操作獲得類似動態語言的特性。Java的動態性讓程式設計更加靈活!

3、Java反射的功能

  (1)在執行時判斷任意一個物件所屬的類。
  (2)在執行時構造任意一個類的物件。
  (3)在執行時判斷任意一個類所具有的成員變數和方法。
  (4)在執行時獲取泛型資訊。
  (5)在執行時呼叫任意一個物件的成員變數和方法。
  (6)在執行時處理註解。
  (7)生成動態代理。

4、相關API

  程式碼示例:類的例項化

 1 class Person {
 2     private String name;
 3     public int age;
 4     public void say() {
 5         System.out.println("say hello");
 6     }
 7 
 8     private String eat(String food) {
 9         System.out.println("我正在吃:" + food);
10         return food;
11     }
12     // getter && setter
13     // 有參、無參構造器
14 }
15 
16 // 未使用反射
17 public class Main {
18     public static void main(String[] args) {
19         // 1.建立類的物件
20         Person person = new Person("Tom", 18);
21         // 2.通過物件,呼叫屬性及方法
22         person.age = 10;
23         System.out.println(person.toString());
24 
25         person.say();
26         // 3.不可以通過物件呼叫私有結構
27         // person.name = "";
28         // person.eat();
29     }
30 }
31 
32 // 使用反射
33 public class Main {
34     public static void main(String[] args) throws Exception {
35         // 1.建立類的物件
36         Class<Person> clazz = Person.class;
37         // 獲取類的構造器
38         Constructor<Person> constructor = clazz.getConstructor(String.class, int.class);
39         Person person = constructor.newInstance("Tom", 12);
40         System.out.println(person.toString());
41 
42         // 2.通過反射,呼叫屬性及方法
43         Field age = clazz.getDeclaredField("age");
44         age.set(person, 10);
45         System.out.println(person.toString());
46 
47         Method show = clazz.getDeclaredMethod("say");
48         show.invoke(person);
49     }
50 }

  程式碼示例:呼叫私有結構

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         // 1.通過反射呼叫私有構造器
 4         Class<Person> clazz = Person.class;
 5         Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class);
 6         constructor.setAccessible(true); // 設定構造器可見
 7 
 8         Person person = constructor.newInstance("Jerry");
 9         System.out.println(person.toString());
10 
11         // 2.通過反射呼叫私有屬性
12         Field name = clazz.getDeclaredField("name");
13         name.setAccessible(true); // 設定屬性可見
14         name.set(person, "mary");
15         System.out.println(person.toString());
16 
17         // 3.通過反射呼叫私有方法
18         Method method = clazz.getDeclaredMethod("eat", String.class);
19         method.setAccessible(true); // 設定方法可見
20         String s = (String) method.invoke(person, "黃燜雞");
21         System.out.println(s);
22     }
23 }

  反射機制與物件導向中的封裝性是不是矛盾的?如何看待這兩個技術?
  不矛盾。可以理解為,封裝:我們已經提供了更好的方法給你(public),而你非要去走彎路(private)也可以,但是很沒有必要。

二、理解Class類並獲取Class例項

1、Class類

  類的載入過程:程式經過javac.exe命令以後,會生成一個或多個位元組碼檔案(.class),接著使用java.exe命令對某個位元組碼檔案進行解釋執行。即:將位元組碼檔案載入到記憶體中,此過程就稱為類的載入。
  載入到記憶體中的類,就稱為執行時類,此執行時類,就作為Class的一個例項。一個載入的類在 JVM 中只會有一個Class例項,換句話說,Class的例項就對應著一個執行時類。
  載入到記憶體中的執行時類,會快取一定的時間。在此時間之內,可以通過不同的方式來獲取此執行時類。

2、獲取Class例項

  程式碼示例:獲取Class例項的4種方式

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         // 方式一:呼叫執行時類的屬性
 4         Class<Person> clazz1 = Person.class;
 5         System.out.println(clazz1);
 6 
 7         // 方式二:通過執行時類的物件
 8         Person person = new Person();
 9         Class<? extends Person> clazz2 = person.getClass();
10         System.out.println(clazz2);
11 
12         // 方式三:呼叫Class的靜態方法
13         Class<?> clazz3 = Class.forName("com.lx.day01.Person");
14         System.out.println(clazz3);
15 
16         // 方式四:使用類的載入器
17         ClassLoader classLoader = Main.class.getClassLoader();
18         Class<?> clazz4 = classLoader.loadClass("com.lx.day01.Person");
19         System.out.println(clazz4);
20 
21         System.out.println(clazz1 == clazz2); // true
22         System.out.println(clazz1 == clazz3); // true
23         System.out.println(clazz1 == clazz4); // true
24     }
25 }

  程式碼示例:Class例項對應的結構

 1 public class Main {
 2     public static void main(String[] args) {
 3         Class<Object> objectClass = Object.class; //
 4         Class<Comparable> comparableClass = Comparable.class; // 介面
 5         Class<String[]> aClass = String[].class; // 陣列
 6         Class<int[][]> aClass1 = int[][].class; // 二維陣列
 7         Class<ElementType> elementTypeClass = ElementType.class; // 列舉
 8         Class<Override> overrideClass = Override.class; // 註解
 9         Class<Integer> integerClass = int.class; // 基本資料型別
10         Class<Void> voidClass = void.class;
11         Class<Class> classClass = Class.class;
12 
13         // 只要元素型別與維度一樣,就是同一個Class
14         int[] a = new int[10];
15         int[] b = new int[100];
16         Class<? extends int[]> aClass2 = a.getClass();
17         Class<? extends int[]> aClass3 = b.getClass();
18         System.out.println(aClass2 == aClass3); // true
19     }
20 }

三、類的載入與ClassLoader的理解

1、類的載入過程

  當程式主動使用某個類時,如果該類還未被載入到記憶體中,則系統會通過如下三個步驟來對該類進行初始化。

  載入:將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構,然後生成一個代表這個類的 java.lang.Class 物件,作為方法區中類資料的訪問入口(即引用地址)。所有需要訪問和使用類資料只能通過這個 Class 物件。這個載入的過程需要類載入器參與。
  連結:將 Java 類的二進位制程式碼合併到 JVM 的執行狀態之中的過程。
  (1)驗證:確保載入的類資訊符合 JVM 規範,例如:以 cafe 開頭,沒有安全方面的問題。
  (2)準備:正式為類變數(static)分配記憶體並設定類變數預設初始值的階段,這些記憶體都將在方法區中進行分配 。
  (3)解析:虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程 。
  初始化:執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集類中所有類變數的賦值動作和靜態程式碼塊中的語句合併產生的(類構造器是構造類資訊的,不是構造該類物件的構造器)。
  當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化 。
  虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步。即:保證一個類只會被載入一次。
  程式碼示例:靜態變數賦值

 1 public class Main {
 2     public static void main(String[] args) {
 3         System.out.println(A.m); // 300
 4     }
 5 }
 6 
 7 class A {
 8     static int m = 100;
 9 
10     static {
11         m = 300;
12     }
13 }
14 // 第二步:連結結束後 m = 0
15 /*
16  * 第三步:初始化後,m 的值由<clinit>方法執行決定。
17  * 這個 A 的類構造器<clinit>()方法由類變數的賦值和靜態程式碼塊中的語句按照順序合併。
18  * 類似於<clinit>(){
19  *             m = 100;
20  *             m = 300;
21  *      }
22  */

2、ClassLoader的理解

  類載入器的作用
  (1)載入類:將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構,然後在堆中生成一個代表這個類的 java.lang.Class 物件,作為方法區中類資料的訪問入口。
  (2)類快取:標準的 JavaSE 類載入器可以按要求查詢類,但一旦某個類被載入到類載入器中,它將維持載入(快取)一段時間。不過 JVM 垃圾回收機制可以回收這些 Class 物件。

  類載入器作用是用來把類(class)載進記憶體的。JVM 規範定義瞭如下型別的類的載入器。

四、呼叫執行時類

1、建立執行時類的物件

  程式碼示例:建立執行時類的物件

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         // 1.獲取Class物件
 4         Class<Person> clazz = Person.class;
 5         // 2.呼叫指定引數的構造器
 6         Constructor<Person> constructor = clazz.getConstructor(String.class, int.class);
 7 
 8         // 3.建立執行時類的物件
 9         Person person = constructor.newInstance("牛牛", 18);
10         System.out.println(person); // Person{name='牛牛', age=18}
11     }
12 }

2、獲取執行時類的結構

  Class類:
  public Class<?>[] getInterfaces():獲取類實現的全部介面。
  public native Class<? super T> getSuperclass():獲取類所繼承的父類。
  public Type getGenericSuperclass():獲取帶泛型的父類。
  public Constructor<?>[] getConstructors():獲取類的所有public構造方法。
  public Constructor<?>[] getDeclaredConstructors():獲取類宣告的所有構造方法。
  public Method[] getMethods():獲取類的所有public方法。
  public Method[] getDeclaredMethods():獲取類宣告的所有方法。
  public Field[] getFields():獲取類的所有public的Field。
  public Field[] getDeclaredFields():獲取類宣告的所有Field。

  Constructor類:
  public int getModifiers():取得修飾符。
  public String getName():取得方法名稱。
  public Class[] getParameterTypes():取得引數的型別。

  Method類:
  public Class<?> getReturnType():取得全部的返回值。
  public Class<?>[] getParameterTypes():取得全部的引數。
  public int getModifiers():取得修飾符。
  public Class<?>[] getExceptionTypes():取得異常資訊。

  Field類:
  public int getModifiers():以整數形式返回此Field的修飾符。
  public Class<?> getType():得到Field的屬性型別。
  public String getName():返回Field的名稱。

3、呼叫執行時類的指定結構

  程式碼示例:同"呼叫私有結構"

五、反射的應用:動態代理

   見《設計模式》

相關文章