17.反射(reflection)【Java溫故系列】

參考自–《Java核心技術卷1》

Java 的反射庫提供了一個非常豐富且精心設計的工具集,以便編寫能夠動態操縱 Java 代碼的程序。這項功能被大量地應用於 JavaBeans 中,它是 Java 組件的體系結構。

能夠分析類能力的程序稱爲反射。反射機制的功能極其強大,它可以用來:

  • 在運行時分析類
  • 在運行時查看對象
  • 實現通用的數組操作代碼
  • 利用 Method 對象

反射是一種功能強大且複雜的機制。


1 Class 類

在程序運行期間,Java 運行時系統始終爲所有的對象維護一個被稱爲運行時的類型標識。這個標識信息跟蹤着每個對象所屬的類。虛擬機利用運行時類型信息找到相應的方法並執行下去。

可以通過專門的 Java 類訪問這些信息(運行時類型),保存這些信息的類被稱爲 Class。

獲取 Class 類對象的方法:

1)Object 類中的 getClass() 方法將會返回一個 Class 類型的實例。

Employee e = new Employee();   //創建一個僱員對象
Manager m = new Manager();    //創建一個經理對象

Class c1 = e.getClass();
Class c2 = m.getClass();

如同用一個 Employee 對象表示一個特定的僱員屬性一樣,一個 Class 對象將表示一個特定類的屬性。最常用的 Class 方法是 getName。這個方法將返回類的名字,例如:

System.out.println(e.getClass().getName()); //輸出 class cn.cbq.study.sample01.Employee
System.out.println(m.getClass().getName()); //輸出 class cn.cbq.study.sample01.Manager

如果類在一個包裏,包的名字也作爲類名的一部分輸出。

2)還可以調用靜態方法 forName 獲取類名對應的 Class 對象:

String className = "java.util.Random";
try {
	Class c = Class.forName(className);
	System.out.println(c.getName());   //輸出 java.util.Random
} catch (ClassNotFoundException ex) {
	ex.printStackTrace();
}     

如果類名保存在字符串中,並可在運行中改變,就可以使用這個方法。當然,這個方法只有在字符串爲類名或接口名時才能夠執行,否則,forName 方法將拋出異常(無論何時使用這個方法,都應該提供一個異常處理器)。

:在啓動 Java 程序時,包含 main 方法的類被加載,它又會加載所有它需要的類。這些被加載的類又要加載它們各自需要的類,以次類推。對於一個大型的應用程序來說,這將消耗很多時間。可以使用這樣一個技巧,給人一種啓動速度比較快的幻覺:在 main 方法中通過調用 Class.forName 手工地加載其他的類,但需確保包含 main 方法的類沒有顯式地引用其他的類。

3)如果 T 是任意的 Java 類型(或 void 關鍵字),T.class 將代表匹配的類對象:

Class c1 = Random.class;  //要先 import java.util.*;否則 c1 = java.util.Random.class;
Class c2 = int.class;   // 輸出 int
Class c3 = Double[].class;   //輸出 class [Ljava.lang.Double;

注意,一個 Class 對象實際上表示的是一個類型,而這個類型未必一定是一種類。例如,int 不是類,但 int.class 是一個 Class 類型的對象。

:Class 類實際上是一個泛型類。例如,Employee.class 的類型是 Class<Employee>。它將抽象的概念更加複雜化了,在大多數實際問題中,可以忽略類型參數,而使用原始的 Class 類。

getName 方法在應用於數組類型的時候會返回比較奇怪的名字:

  • Double[].class.getName() 返回 “[Ljava.lang.Double”
  • int[].class.getName() 返回 “[I”

虛擬機爲每個類型管理一個 Class 對象。可以使用 == 運算符實現兩個類對象比較的操作:

if(e.getClass()==Employee.class)  ...

還有一個很有用的方法 newInstance() ,可以用來動態地創建一個類的實例:

Employee e = new Employee();
try {
	Employee es = e.getClass().newInstance();   //創建了一個新的Employee實例 es
} catch (InstantiationException ex) {
	ex.printStackTrace();
} catch (IllegalAccessException ex) {
	ex.printStackTrace();
}

創建了一個與 e 具有相同類類型的實例。 newInstance() 方法調用默認的構造器(沒有參數的構造器)初始化新創建的對象。若這個類沒有默認的構造器(有其他帶參數的構造器),就會拋出一個異常。

forNamenewInstance() 配合起來使用,就可以根據存儲在字符串中的類名創建一個對象:

String className = "java.util.Random";
try {
	Object obj = Class.forName(s).newInstance();  //創建一個Random類對象
	Random ran = (Random)obj;
    int i = ran.nextInt();   //使用 Random 對象生成一個隨機 int 類型數值
} catch (InstantiationException ex) {
	ex.printStackTrace();
} catch (IllegalAccessException ex) {
	ex.printStackTrace();
} catch (ClassNotFoundException ex) {
	ex.printStackTrace();
}

:如果需要以這種方式向希望按名稱創建的類的構造器提供參數(調用帶參數的構造函數),就需要使用 Constructor 類中的 newInstance() 方法。


2 利用反射分析類的能力(檢查類的結構)

在 java.lang.reflect 包中有三個類 Field,Method 和 Constructor 分別用於描述類的域、方法和構造器。

這三個類都有一個叫 getName 的方法,用來返回項目的名稱。Field 類有一個 getType 方法,用來返回描述域所屬類型的 Class 對象。Method 和 Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。這三個類還有一個叫 getModifies 的方法,它將返回一個整型數值,用不同的位開關描述 public 和 static 這樣的修飾符使用狀況。另外,還可以利用 java.lang.reflect 包中的 Modifier 類的靜態方法分析 getModifies 返回的整型數值。例如,可以使用 Modifier 類中的 isPublicisPrivateisFinal 判斷方法或構造器是否是 public、private 或 final。另外還可以利用 Modifier.toString 方法將修飾符打印出來。

Class 類中的 getFieldsgetMethodsgetConstructors 方法將分別返回類提供的 public 域、方法和構造器數組,其中包括超類的公有成員。Class 類的 getDeclaredFieldsgetDeclaredMethodsgetDeclaredConstructors 方法將分別返回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。

下面的案例通過輸入 Java 類 分析 域,方法和構造器:

import java.util.*;
import java.lang.reflect.*;

public class ReflectionTest {

   public static void main(String[] args) {
        // read class name from command line args or user input
        String name;
        if (args.length > 0) {
           name = args[0];
        }else {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter class name (例如:java.util.Date): ");
            name = in.next();
        }

        try {
            // print class name and superclass name (if != Object)
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.print("class " + name);
            if (supercl != null && supercl != Object.class) 
                System.out.print(" extends " + supercl.getName());
            System.out.print("\n{\n");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    /**
     * Prints all constructors of a class
     * @param cl a class
     */
    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String name = c.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.print(name + "(");

            // print parameter types
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) 
                    System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all methods of a class
     * @param cl a class
     */
    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();

        for (Method m : methods) {
            Class retType = m.getReturnType();
            String name = m.getName();

            System.out.print("   ");
            // print modifiers, return type and method name
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.print(retType.getName() + " " + name + "(");

            // print parameter types
            Class[] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) 
                    System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all fields of a class
     * @param cl a class
     */
    public static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();

        for (Field f : fields) {
            Class type = f.getType();
            String name = f.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(f.getModifiers());
            if (modifiers.length() > 0) 
                System.out.print(modifiers + " ");
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}

相關的API:

1)Class 類:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vUCRWdie-1590042929698)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20200502155909356.png)]

2)Field、Method 和 Constructor 類:

在這裏插入圖片描述

3)Modifier 類:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MBhPpXJn-1590042929703)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20200502160042760.png)]


3 在運行時使用反射分析對象

通過上面的內容,已經知道了如何查看任意對象的數據域名稱和類型:

1.獲取對應的 Class 對象

2.通過 Class 對象調用 getDeclaredFields

那麼數據域的實際內容又怎麼查看呢?

在編寫程序時,如果知道想要查看的域名和類型,查看指定的域是一件很容易的事。利用反射機制可以查看在編譯時還不清楚的對象域。

查看對象域的關鍵方法是 Field 類中的 get 方法。如果 f 是一個 Field 類型的對象,obj 是某個包含 f 域的類的對象,f.get(obj) 將返回一個對象,其值爲 obj 域的當前值。如下:

//Employee
public class Employee{
	private String name;
    private double salary;
    ...
}
Employee e = new Employee("zs",1000);
Class c = e.getClass();
try {
	Field f = c.getDeclaredField("name");  //獲取 e 對象的 name 域
	Object obj = f.get(e);
	System.out.println(obj);
} catch (NoSuchFieldException | IllegalAccessException ex) {
	ex.printStackTrace();
}
//若 name 是公有域,則成功獲取  zs

實際上,這段代碼存在一個問題:由於 name 域是一個私有域,所以 get 方法將會拋出異常。只有利用get 方法才能得到可訪問域的值。 除非擁有訪問權限,否則 Java 安全機制只允許查看任意對象有哪些域,而不允許讀取它們的值。

反射機制默認行爲受限於 Java 的訪問控制。然而,如果一個 Java 程序沒有受到安全管理器的控制,就可以覆蓋訪問控制。爲了達到這個目的,需要調用 Field、Method 或 Constructor 對象的 setAccessible 方法:

f.setAccessible(true);
f.get(e);   //即使 name 域是私有域,也可以獲得 name 域的值

setAccessible 方法是 AccessibleObject 類中的一個方法,它是 Field、Method 或 Constructor 類的公共超類。

get 方法還有一個需要解決的問題:name 域是一個 String ,因此把它作爲 Object 返回沒有什麼問題;但是,若要查看 salary 域,它屬於 double 類型,而 Java 的數值域不是對象,就無法直接查看(拋出異常)。

要想解決這個問題,可以使用 Field 類中的 getDouble 方法,此時,反射機制將會自動地將這個域值打包到相應的對象包裝器中,這裏將打包成 Double。

當然,可以獲得就可以設置。調用 f.set(obj,value) 將 obj 對象的 f 域設置成新值 value。

 f.set(e,"lisa");  //將上述的e對象的name域設置爲"lisa"

在這裏插入圖片描述

4 使用反射編寫泛型數組

java.lang.reflect 包中的 Array 類允許動態地創建數組。例如 Array 類的 copyOf 方法:

Employee[] e = new Employee[100];
...
//當數組e已經被填滿後
e = Arrays.copyOf(a,2*a.length);

顯然,copyOf 方法是一個通用的方法,那麼它返回的數組必須爲 Object 數組,那麼它是如何實現的呢?

首先,我們知道,Employee[] 可以臨時地轉換爲 Object[] 數組,然後將它轉換回來也是可以的;但一個一開始就是 Object[] 的數組卻無法轉換成 Employee[] 數組(包括其他類型的數組)。爲了編寫通用的 copyOf 函數,就需要能夠創建與原數組類型相同的新數組。

爲此,就需要 java.lang.reflect 包中的 Array 類的一些方法。其中最關鍵的是 Array 類中的靜態方法 newInstance ,它能構造新數組。在調用它是必須提供兩個參數:原數組的元素類型,新數組的長度。

Object newArray = Array.newInstance(componentType,newLength);

可以通過 Array.getLength(obj) 獲得當前數組 obj 的長度;而要獲取原數組的元素類型以設置新數組的元素類型,就需要進行以下工作:

  • 首先獲取原數組的類對象
  • 確認它是一個數組
  • 使用 Class 類(只能定義表示數組的類對象)的 getComponentType 方法確定數組對應的類型。

copyOf 方法的實現如下:

public static Object copyOf(Object obj,int newlength){
	Class c1 = obj.getClass();
    if(!c1.isArray())
        return null;
    Class componentType = c1.getComponentType();
    int length = Array.getLength(obj);
    Object newArray = Array.newInstance(componentType,newLength);
    //public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);原數組,原數組起始位置,目標數組,目標數組開始位置,要copy的數組(原數組的)長度
    System.arraycopy(obj,0,newArray,0,Math.min(length,newLength));
    return newArray;
}

copyOf 方法可以用來擴展任意類型的數組,而不僅僅是對象數組:

int[] a = {1,2,3,4,5};
a = (int[])copyOf(a,10);

整型數組類型 int[] 可以被轉換成 Object,但不能轉換成對象數組。

在這裏插入圖片描述


5 調用任意方法

反射機制允許 Java 程序員調用任意方法。

在 Method 類中有一個 invoke 方法,它允許調用包裝在當前 Method 對象中的方法,invoke 方法的簽名是:

Object invoke(Object obj,Object... args)

第一個參數是隱式參數,其餘參數是對象提供的顯式參數。

對於靜態方法,第一個參數可以被省略,即可以將它設置爲 null。

例如,假設 m1 代表 Employee 類的 getName 方法,下面的語句顯示瞭如何調用這個方法:

Employee e = new Employee("zs",1000);
Method m1 = null;
Method m2 = null;
try {
	m1 = Employee.class.getMethod("getName");
    m2 = Employee.class.getMethod("getSalary");
	String name = (String) m1.invoke(e);
    double s = (Double)m2.invoke(e);
	System.out.println(name);
} catch (Exception ex) {
	ex.printStackTrace();
}

如果返回類型是基本類型,invoke 方法會返回其包裝器類型。例如,假設有 m2 表示 Employee 的 getSalary 方法,那麼實際返回的對象實際上是一個 Double ,必須相應地完成類型轉換可以使用自動拆箱將它轉換爲一個 double.

getMethod 方法的簽名是:

Method getMethod(String name,Class... parameterTypes)

parameterTypes 是傳入參數的類型。

調用靜態方法時:

Method m = null;
try {
	m = Math.class.getMethod("sqrt",double.class);
	double x = (Double)m.invoke(null,4);
	System.out.println(x);   //輸出 2.0
} catch (Exception ex) {
	ex.printStackTrace();
}

invoke 的參數和返回值都必須爲 Object 類型的,這意味着必須進行多次的類型轉換。這樣做將會使編譯器錯過檢查代碼錯誤的機會,從而只有在測試階段才能發現這些錯誤。不但如此,使用反射獲得方法指針的代碼要比直接調用方法明顯慢一些。

hod(String name,Class... parameterTypes)

parameterTypes 是傳入參數的類型。

調用靜態方法時:

Method m = null;
try {
	m = Math.class.getMethod("sqrt",double.class);
	double x = (Double)m.invoke(null,4);
	System.out.println(x);   //輸出 2.0
} catch (Exception ex) {
	ex.printStackTrace();
}

invoke 的參數和返回值都必須爲 Object 類型的,這意味着必須進行多次的類型轉換。這樣做將會使編譯器錯過檢查代碼錯誤的機會,從而只有在測試階段才能發現這些錯誤。不但如此,使用反射獲得方法指針的代碼要比直接調用方法明顯慢一些。

建議僅在有必要的時候才使用 Method 對象,最好使用接口以及 lambda 表達式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章