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.為什麼説反射會影響程序性能?

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