Java進階--深入理解Java的反射機制

語言: CN / TW / HK

在上篇文章《深入JVM--探索Java虛擬機器的類載入機制》中我們深入探討了JVM的類載入機制。我們知道,在例項化一個類時,如果這個類還沒有被虛擬機器載入,那麼虛擬機器會先執行類載入過程,將該類所對應的位元組碼讀取到虛擬機器,並生成一個與這個類對應的Class物件。而在類載入的過程中,由於有雙親委派機制的存在,虛擬機器保證了同一個類會被同一個類載入器所載入,進而保證了在虛擬機器中只存在一個被載入類所對應的Class例項。而這個Class例項與我們今天要講的反射有著莫大的關係。

一、Java反射機制概述

在學習反射之前,我們先來搞清楚幾個概念:

  • Class類是什麼?
  • Class物件是什麼?
  • Class物件與我們的Java類有什麼關係?

假設現在有一個Person類,程式碼如下:

package com.test.reflection;

public class Person {
	
    private String name;
    protected int age;
    public String sex;

    private Person() {

    }

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

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

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

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private void testPrivateMethod() {
        System.out.println("testPrivateMethod被呼叫");
    }
}
複製程式碼

你是否能通過Person類來解釋清楚上邊提到的三個問題呢?我們不妨接著往下看。

1.Class類與Class物件

提到Class類,大家多多少少都應該有些接觸,即使你沒有使用過反射,也不可避免的接觸到Class類。例如,在Android中進行Activity頁面跳轉的時候,我們需要一個Intent,而例項化Intent則需用到Intent的構造方法,程式碼如下:

public Intent(Context packageContext, Class<?> cls) {
        mComponent = new ComponentName(packageContext, cls);
    }
複製程式碼

可以看到,Intent的構造方法中的第二個引數,接受的就是一個Class物件。到這裡,我們應該都明白,Class就是JDK為我們提供的一個普普通通的Java類,它跟我們自己定義一個Person類其實並無任何本質上的區別。我們進入Class類的原始碼可以看到,Class類是一個泛型類,並且它實現了若干個介面,原始碼如下:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {
	// ...省略主體程式碼
}
複製程式碼

既然Class就是一個普普通通的Java類,那在使用它的時候一定需要例項化出來一個Class物件。但奇怪的是,我們在平時寫程式碼的時候好像從來沒有通過new關鍵字來例項化過Class物件?那它到底是在哪裡被例項化的呢?瞭解類載入機制的同學想必應該都清楚,當然,我們在文章開頭也已經提到了,Class物件是在類載入的時候由虛擬機器自動生成的。

我們以上邊的Person類為例,當我們使用new關鍵字例項化Person物件的時候,如果Person類的位元組碼還沒有被載入到虛擬機器,那麼虛擬機器首先啟動類載入器將Person類的位元組碼讀取到虛擬機器中,併為其生成一個Class<Person>的例項,而類載入器的雙親委派模型保證了虛擬機器中只會生成一個Class<Person>的例項。而如果在例項化Person物件的時候,Person已經被載入到了虛擬機器,則無需再進行Person的類載入過程,直接例項化Person即可。到這裡,我們似乎可以感覺到Person物件跟Class<Person>一定存在著某種關係。我們接著往下看。

2.Person類與Class<Person>物件的關係

現在,我們回想一下我們在進行Activity頁面跳轉的時候Intent構造方法的第二個引數傳的是什麼呢?是不是像下邊這樣:

	Intent intent=new Intent(this,MainActivity.class);
複製程式碼

通過MainActivity.class我們可以得到MainActivity對應的Class物件:

	Class<MainActivity> mainActivityClass = MainActivity.class;
複製程式碼

而mainActivityClass 物件就是虛擬機器在載入MainActivity的時候生成的,並且虛擬機器保證了mainActivityClass在虛擬機器中是唯一的。 這一過程對於Person類也是一樣的,我們可以通過Person .class來拿到虛擬機器中唯一的一個Class<Person>例項。

	Class<Person> personClass=Person.class;
複製程式碼

另外,我們還可以通過Person 的例項物件來獲得Class<Person>物件,如下:

	Person person=new Person();
	Class<Person> personClass=(Class<Person>)person.getClass();
複製程式碼

當然,除了上述兩種方法之外,我們還可以通過Person類的包名來獲得Class<Person>的例項,程式碼如下:

	try {
			Class<Person> personClass=(Class<Person>)Class.forName("com.test.reflection");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
複製程式碼

由於Class<Person >物件在虛擬機器中是唯一的,那麼上述三種方法獲取到的Class<Person >一定是同一個例項。

二、什麼是反射?

好了,上邊囉嗦了這麼多,終於到了正題了。那到底什麼是反射呢?我們來看下百度百科給出的定義:

Java的反射(reflection)機制是指在程式的執行狀態中,可以構造任意一個類的物件,可以瞭解任意一個物件所屬的類,可以瞭解任意一個類的成員變數和方法,可以呼叫任意一個物件的屬性和方法。這種動態獲取程式資訊以及動態呼叫物件的功能稱為Java語言的反射機制。

雖然上述定義對反射的描述已經非常清楚。但是對於沒有了解過反射的同學來說看了之後可能還是一頭霧水。下面,在第一章的基礎上來說下我來說下我對反射的理解:

在Java中,所有已經被虛擬機器的類載入器載入過的類(稱為T)都會在虛擬機器中生成一個唯一的與T類所對應的Class<T>物件。在程式執行時,通過這個Class<T>物件,我們可以例項化出來一個T物件;可以通過Class<T>物件訪問T物件中的任意成員變數,呼叫T物件中的任意方法,甚至可以對T物件中的成員變數進行修改。我們將這一系列操作稱為Java的反射機制。

到這裡我們發現,其實Java的反射也沒有那麼神祕了。說白了就是通過Class物件來操控我們的物件罷了。因此,接下來我們想要弄懂反射只需要來詳細的認識一下Class這個類給我們提供的API即可。

1.Java反射相關類

我們知道,一個Java類可以包含成員變數、構造方法、以及普通方法。同時,我們又知道Java是一種很純粹的面向物件的語言。在Java語言中,萬物皆物件,類的成員變數、構造方法以及普通方法在Java中也被封裝成了物件。它們分別對應Field類、Constructor類以及Method類。這幾個類與反射息息相關。因此,在開始之前,我們需要先了解下這幾個與反射相關的類,如下圖: 在這裡插入圖片描述

  • Field 類:位於Java.lang.reflect包下,在Java反射中Field用於獲取某個類的屬性或該屬性的屬性值。
  • Constructor 類: 位於java.lang.reflect包下,它代表某個類的構造方法,用來管理所有的建構函式的類。
  • Method 類: 位於java.lang.reflect包下,在Java反射中Method類描述的是類的方法資訊(包括:方法修飾符、方法名稱、引數列表等等)。
  • Class 類: Class類在上文我們已經多次提到,它表示正在執行的 Java 應用程式中的類的例項。
  • Object 類: Object類大家應該都比較熟悉了。它是所有 Java 類的父類。所有物件都預設實現了 Object 類的方法。在Object物件中,可以通過getClass()來獲得該類對應的Class例項。

2.Java反射常用API

通過上文我們已經知道,所謂的反射,其實就是通過API操作Class物件。因此,在進行反射操作的第一步我們應該首先拿到Class的例項。在第一種中我們已經知道可以通過三種方式來獲得Class的例項。以獲取Person類的Class物件為例,三種方法分別如下:

	// 通過類的class獲得Class例項
	Class<Person> personClass=Person.class;
	// 通過類的包名獲得Class例項
	Class<Person> personClass=(Class<Person>)Class.forName("com.test.reflection");
	// 通過物件獲得Class例項
	Person person=new Person();
	Class<Person> personClass=(Class<Person>)person.getClass();
複製程式碼

在拿到Person類的Class例項後,我們就可以通過Class例項獲取到Person類中的任意成員,包括構造方法、普通方法、成員變數等。

2.1獲取所有構造方法

Class類中為我們提供了兩個獲取構造方法的API,這兩個方法如下:

  • Constructor[] getDeclaredConstructors() 用於獲取當前類中所有構造方法。但不包括包括父類中的構造方法。
  • Constructor[] getConstructors() 用於獲取本類中所有public修飾的構造方法,不包括父類的構造方法。

(1)getDeclaredConstructors()

以Person類為例,我們來先來嘗試getDeclaredConstructors方法的使用:

		Class<Person> personClass=Person.class;
		Constructor[] declaredConstructors= personClass.getDeclaredConstructors();
		for(Constructor declaredConstructor:declaredConstructors) {
			System.out.println(declaredConstructor);
		}
複製程式碼

注意,我們在Person類中聲明瞭兩個構造方法,其中無參構造方法是一個私有的構造方法。我們來看下上述程式碼的列印結果:

private com.test.reflection.Person() public com.test.reflection.Person(java.lang.String,int)

可以看到,getDeclaredConstructors方法可以獲取到類中包括私有構造方法在內的所有構造方法。

(2) getConstructors()

接著我們將getDeclaredConstructors()方法換成getConstructors()方法:

		Class<Person> personClass=Person.class;
		Constructor[] declaredConstructors= personClass.getConstructors();
		for(Constructor declaredConstructor:declaredConstructors) {
			System.out.println(declaredConstructor);
		}
複製程式碼

再來看輸出結果:

public com.test.reflection.Person(java.lang.String,int)

此時,只有被聲明瞭public的方法被列印了出來。

2.2 獲取指定構造方法

在Class中同樣提供了兩個獲取指定構造方法的API,如下:

  • Constructor getDeclaredConstructor(Class<?>... parameterTypes) 該方法用來獲取類中任意的構造方法,包括private修飾的構造方法。無法通過該方法獲取到父類的構造方法。
  • Constructor getConstructor(Class<?>... parameterTypes) 該方法只能用來獲取該類中public的構造方法。無法獲取父類中的構造方法。

我們可以嘗試使用getDeclaredConstructor方法來獲取Person的私有構造方法與public的有參構造方法:

       try {
            Constructor<Person> declaredConstructor = personClass.getDeclaredConstructor();
            Constructor<Person> declaredConstructor2 = personClass.getDeclaredConstructor(String.class,int.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
複製程式碼

而如果使用getConstructor獲取私有方法,則會丟擲java.lang.NoSuchMethodException。

2.3 使用反射例項化物件

通過反射例項化物件有多種途徑,可以使用Class的newInstance方法,同時也可以使用Constructor類。

  • 通過Class的newInstance例項化Person
  • 使用Constructor例項化物件

(1)通過Class的newInstance例項化物件 這種方式使用起來非常簡單,直接呼叫newInstance方法即可完成物件的例項化。程式碼如下:

		Class<Person> personClass = Person.class;
        try {
            Person person = personClass.newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
複製程式碼

但是,通過這一方法有一定的侷限性。即只能例項化無參構造方法的類,同時這個無參構造方法不能使用private修飾。否則會丟擲異常。這個方法在Java 9中已經被宣告為Deprecated,並且推薦使用Constructor來例項化物件。

(2) 使用Constructor例項化物件

	try {
            Constructor<Person> declaredConstructor = personClass.getDeclaredConstructor(String.class, int.class);
            Person ryan = declaredConstructor.newInstance("Ryan", 18);
            System.out.println(ryan.getName() + "---" + ryan.getAge());
        } catch (NoSuchMethodException | InvocationTargetException
                | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
複製程式碼

如上程式碼,我們通過Person的有參Constructor例項化出來一個Person類,並輸出如下結果:

Ryan---18

而通過Constructor例項化私有的構造方法時,需要通過Constructor的setAccessible(true)來使Constructor可見,進而進行例項化。否則則會丟擲IllegalAccessException異常。例項化私有構造方法的程式碼如下:

	try {
            Constructor<Person> declaredConstructor = personClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            Person ryan = declaredConstructor.newInstance();
        } catch (NoSuchMethodException | InvocationTargetException
                | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
複製程式碼

2.4 使用反射獲取類的所有成員變數

Class同樣提供了兩方法來獲取類的成員變數,分別如下:

  • getDeclaredFields() 獲取該類中所有成員變數,無法獲取到從父類中繼承的成員變數。
  • getFields() 獲取類中所有public的成員變數,包括從父類中繼承的public的成員變數。

(1)首先通過getDeclaredFields()來獲取Person的成員變數,程式碼如下:

		Class<Person> personClass = Person.class;
        Field[] declaredFields = personClass.getDeclaredFields();
        for (Field field : declaredFields) {
            System.out.println(field.toString());
        }
複製程式碼

輸出結果為:

private java.lang.String com.test.reflection.Person.name
protected int com.test.reflection.Person.age
public java.lang.String com.test.reflection.Person.sex

可以看到,無論時private修飾的成員變數還是public修飾的成員變數都通過getDeclaredFields獲取到。 (2)接著來看getFields()方法:

		Class<Person> personClass = Person.class;
        Field[] fields = personClass.getFields();
        for (Field field : fields) {
            System.out.println(field.toString());
        }
複製程式碼

輸出結果為:

public java.lang.String com.test.reflection.Person.sex

可以看到,通過getFields方法只獲取到了Person類中public的成員變數。

2.5 反射獲取並修改類的成員變數

可以通過getDeclaredField方法與getField獲取類中從成員變數,區別如下:

  • getDeclaredField(String) 獲取該類任意修飾符的成員變數,但不包括從父類中繼承的成員變數。
  • getField(String) 獲取該類任意public修飾的成員變數,包括從父類中繼承的public的成員變數。

獲取Person類的私有成員變數,並通過反射來修改Person物件中的私有變數name,程式碼如下:

        Class<Person> personClass = Person.class;
        Person person = new Person("Ryan", 18);
        try {
            System.out.println("反射修改前name為:" + person.getName());
            // 獲取Person中的私有成員變數name
            Field name = personClass.getDeclaredField("name");
            // 將name設定為可見
            name.setAccessible(true);
            // 修改person例項中name的值
            name.set(person, "Helen");
            System.out.println("反射修改後name為:" + person.getName());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

複製程式碼

輸出結果如下:

反射修改前name為:Ryan
反射修改後name為:Helen

2.6 反射獲取類中的所有方法

  • getDeclaredMethods() 獲取本類中所有方法,不包括從父類中繼承的方法。
  • getMethods() 獲取類中所有public方法,包括從父類中繼承的public方法。

(1)getDeclaredMethods()

通過getDeclaredMethods獲取Person中的所有方法(不包括父類):

        Class<Person> personClass = Person.class;
        // 獲取類中的所有方法,包括私有方法,但不包括父類中的方法。
        Method[] declaredMethods = personClass.getDeclaredMethods();
        // 遍歷並列印方法資訊
        for (Method method :declaredMethods) {
            System.out.println("getDeclaredMethods:"+method.toString());
        }
複製程式碼

輸出結果如下:

getDeclaredMethods:public java.lang.String com.test.reflection.Person.getName() getDeclaredMethods:public int com.test.reflection.Person.getAge() getDeclaredMethods:private void com.test.reflection.Person.testPrivateMethod()

(2)getMethods

通過getMethods獲取Person中的所有public方法(包括父類):

        Class<Person> personClass = Person.class;
        //	獲取Person類中的所有方法,包括父類的方法
        Method[] methods = personClass.getMethods();
        // 遍歷methods並列印方法資訊
        for (Method method : methods) {
            System.out.println("getMethods:"+method.toString());
        }
複製程式碼

輸出結果如下:

 getMethods:public java.lang.String com.test.reflection.Person.getName() 
 getMethods:public int com.test.reflection.Person.getAge()    
 getMethods:public final void java.lang.Object.wait(long,int) throws   java.lang.InterruptedException       
 getMethods:public final void java.lang.Object.wait() throws java.lang.InterruptedException
 getMethods:public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException              
 getMethods:public boolean java.lang.Object.equals(java.lang.Object)        
 getMethods:public java.lang.String java.lang.Object.toString()         
 getMethods:public native int java.lang.Object.hashCode()      
 getMethods:public final native java.lang.Class java.lang.Object.getClass()
 getMethods:public final native void java.lang.Object.notify()
 getMethods:public final native void java.lang.Object.notifyAll()
複製程式碼

2.7 使用反射呼叫物件的方法

(1) 反射呼叫物件的私有方法

通過反射呼叫Person類中的私有方法testPrivateMethod,程式碼如下:

        try {
            // 獲取Person類中的私有方法testPrivateMethod
            Method testPrivateMethod = personClass.getDeclaredMethod("testPrivateMethod");
            // 將testPrivateMethod方法設定為可見
            testPrivateMethod.setAccessible(true);
            // 反射呼叫testPrivateMethod方法
            testPrivateMethod.invoke(person);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
複製程式碼

輸出結果為:

testPrivateMethod被呼叫

三、關於反射的一些問題

1.是否可以通過反射修改final型別的成員變數?

在寫Java程式碼的時候,如果我們將一個成員變數宣告為了final型別,那麼就必須在宣告時候或者在構造方法中為其賦初始值,否則程式是無法編譯通過的。那我們是否可以通過反射來修改final型別的成員變數呢?不妨來嘗試一下。我們將Person中的sex改為private與final修飾,併為其賦初始值為“male"。

public class Person {
	private final String sex ="male";
	public String getSex() {
    	return sex;
    }
	// ...省略其它程式碼
}
複製程式碼

下面通過反射來嘗試將sex的值修改為”female",程式碼如下:

		Class<Person> personClass = Person.class;
        Person person = new Person("Ryan", 18);
        try {
            Field sex = personClass.getDeclaredField("sex");
            System.out.println("修改性別前:" + person.getSex());
            sex.set(person, "female");
            System.out.println("修改性別後:" + person.getSex());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
複製程式碼

執行之後程式並未報錯,輸出結果如下:

修改性別前:male
修改性別後:male

可以看到,我們通過反射並沒有成功修改sex的值,這意味著final修飾的成員變數無法通過反射修改嗎?這倒未必。我們接著來看下邊的程式碼。

public class Person {
	private final Object mObject = new Object();
	public Object getObject() {
    	return mObject ;
    }
	// ...省略其它程式碼
}
複製程式碼

我們在Person中新增一個Object的成員變數,並將其宣告為private final。接下來仍然通過反射來嘗試修改mObject的值。程式碼如下:

Class<Person> personClass = Person.class;
        Person person = new Person("Ryan", 18);
        try {
            Field object = personClass.getDeclaredField("mObject");
            System.out.println("修改Object前:" + person.getObject().toString());
            Object newObject = new Object();
            System.out.println("newObject:" + newObject.toString());
            object.setAccessible(true);
            object.set(person, newObject);
            System.out.println("修改Object後:" + person.getObject().toString());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
複製程式碼

輸出結果如下:

修改Object前:java.lang.Object@4926097b
newObject:java.lang.Object@762efe5d
修改Object後:java.lang.Object@762efe5d

有沒有很奇怪?上邊的String通過反射修改沒有成功,而將程式碼換成Object之後,同樣的程式碼,Object的成員變數卻被修改成功了,這是怎麼回事呢?其實,瞭解Java編譯的同學應該比較清楚。編譯器在編譯Java檔案的時候會將final修飾的基本型別和String優化為一個常量。我們來看下反編譯後的class檔案就明白了。我們將Person編譯的class檔案在AS中開啟,如下圖: 在這裡插入圖片描述

可以清楚的看到在class檔案中getSex方法返回的是一個“male"字面量,而getObject返回的卻是mObject。所以,即使通過程式碼將final修飾的String型別修改成功,在get的時候由於編譯器的優化無法拿到修改後的值。

通過上邊的例子可以確定通過反射是能夠修改final修飾的成員變數的。只是如果該成員變數是基本資料型別或者String型別會被編譯器優化成字面量,從而無法獲得修改後的值。

2.為什麼說反射會影響程式效能?

在專案開發中,我們在能不使用反射的情況下就不使用反射,因為反射會影響程式的效能。這是我們大家都熟知的。但是你知道為什麼說反射會影響程式的效能嗎?要解開這一個問題就需要我們深入反射的原始碼來檢視反射過程中都做了什麼操作。由於這塊內容比較龐大,限於篇幅,關於反射影響效能的問題將在下一篇文章中詳細分析。敬請期待。