深入理解Java反射(一)

dreamGong發表於2018-07-29

1、什麼是反射?

我們先給一個比較官方的說法:反射(Reflection)是Java 程式開發語言的特徵之一,它允許執行中的 Java 程式獲取自身的資訊,並且可以操作類或物件的內部屬性。
簡單的說就是:
通過反射,我們可以在執行時獲得程式或程式集中每一個型別的成員和成員的資訊。
我們知道我們在寫程式碼的時候,物件型別一般都是在編譯期確定下來的。反射機制允許我們動態的建立物件並且呼叫其相關屬性。即使型別在編譯期是未知的,反射機制也能通過載入型別資訊,建立相關物件。 反射的核心是JVM在執行時才動態載入類或呼叫方法/訪問屬性,它不需要事先(寫程式碼的時候或編譯期)知道執行物件是誰。

2、Java反射框架提供的相關功能

  • 在執行時判斷任意一個物件所屬的類;
  • 在執行時構造任意一個類的物件;
  • 在執行時判斷任意一個類所具有的成員變數和方法(通過反射甚至可以呼叫private方法);
  • 在執行時呼叫任意一個物件的方法。

判斷任意一個物件所屬的類(獲取Class物件)

有3種方法可以獲取Class物件,我們來依次介紹

Class類的Class.forName靜態方法。

Class<?> personClass=Class.forName("com.learn.example.Person");
複製程式碼

直接獲取某一型別的class

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

通過呼叫物件的getClass()方法

Person createPerson=new Person();
personClass=createPerson.getClass();
複製程式碼

判斷是否是某一個類的例項

通常這類操作我們是通過instanceof關鍵字來進行判斷的,不過通過Class物件,我們可以通過isInstance方法來進行判斷。

Class<?> personClass=Person.class;
personClass.isInstance(obj);
複製程式碼

通過isInstance方法我們可以判斷obj物件是否是Person類的例項。

通過反射來建立例項

通過反射來建立例項主要有兩種方法。

使用Class物件的newInstance()方法來建立Class物件對應類的例項

Class<?> personClass=Person.class;
Person createPerson;
createPerson=(Person)personClass.newInstance();
複製程式碼

注意:通過這種方法只能呼叫預設的建構函式。

先通過Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法來建立例項。這種方法可以用指定的構造器構造類的例項。

//呼叫無參建構函式
createPerson=(Person)personClass.getConstructor().newInstance();
createPerson.sayInfo();
//呼叫有兩個引數的建構函式
createPerson=(Person)personClass.getConstructor(String.class,int.class).newInstance("gong",27);
createPerson.sayInfo();
複製程式碼

通過這種方式我們既可以呼叫預設的建構函式,也可以呼叫指定的建構函式,這種方式的靈活性更好一些。

獲取一個類的方法

在討論這個議題之前,我們先把Person類的程式碼貼出來

class Person {
    private String name;
    private int age;
    
    public Person() {
    	this("init",88);
    }
    
    public Person(String name,int age) {
    	this.name=name;
    	this.age=age;
    }
    
    private void setName(String name) {
    	this.name=name;
    }
    
    private void setAge(int age) {
    	this.age=age;
    }
    
    public void sayInfo() {
    	System.out.println("name:"+this.name);
    	System.out.println("age:"+this.age);
    }
}
複製程式碼

獲取一個類的方法集合,有三種方法。

getDeclaredMethods()方法返回類或介面宣告的所有方法,包括公共、保護、預設(包)訪問和私有方法,但不包括繼承的方法。

//不包含繼承的方法
Method[] declareMethods=personClass.getDeclaredMethods();
if(declareMethods!=null &&declareMethods.length>0) {
    for (Method method : declareMethods) {
        System.out.println(method);
    }
}
複製程式碼

我們看輸出:

public void com.learn.example.Person.sayInfo()
private void com.learn.example.Person.setAge(int)
private void com.learn.example.Person.setName(java.lang.String)
複製程式碼

從輸出結果我們看到,getDeclaredMethods()方法只會返回當前類或介面定義的所有方法。

getMethods()方法返回某個類的所有公用(public)方法,包括其繼承類的公用方法。

//類的所有公用(public)方法,包括其繼承類的公用方法
Method[] methods=personClass.getMethods();
for (Method method : methods) {
    System.out.println(method);
}
複製程式碼

我們看輸出:

public void com.learn.example.Person.sayInfo()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
複製程式碼

從結果裡面我們看到,getMethods()方法只返回Person類中public方法sayInfo()。剩下的就是繼承自Object基類的所有public方法。

獲取一個類的欄位

Java反射機制也提供了相關方法來獲取一個類的相關欄位。
主要是這幾個方法:

  • getFiled: 訪問公有的成員變數
  • getDeclaredField:類中已宣告的private成員變數。但不能得到其父類的成員變數
  • getFileds和getDeclaredFields用法同上(參照Method)
    請看下面的例子:
Field field=personClass.getDeclaredField("name");
//允許訪問private欄位
field.setAccessible(true);
field.set(createPerson, "XXXXXXX");
createPerson.sayInfo();
複製程式碼

我們通過getDeclaredField方法來動態修改了例項的name屬性值。這樣輸出的時候createPerson物件的name屬性值就被我們動態的修改為“XXXXXXX”了。

在執行時呼叫任意一個物件的方法(invoke方法)

我們前面通過getMethods()方法和getDeclaredMethods()方法獲取了類的方法列表。其實反射還支援動態執行類方法,下面我們通過例子來看下:

//動態執行方法
Method method=personClass.getDeclaredMethod("setName",String.class);
method.setAccessible(true);
method.invoke(createPerson,"WWWWW");
createPerson.sayInfo();
複製程式碼

我們在例子裡面,我們看到我們通過getDeclaredMethod方法來動態呼叫setName方法。第一個引數是方法名,後面的引數是方法的引數。細心的朋友可能發現setName是private方法啊,這也能呼叫?的確,預設情況是不能呼叫的。但是我們可以通過setAccessible來設定是否允許呼叫private(私有)的方法。
注意: getDeclaredMethod()方法和getDeclaredMethods()方法功能是對應的,這個方法只能獲取到類自己定義的方法。 getMethod()方法和getMethods()方法功能是對應的。這個方法只能訪問所有的public方法。

使用反射來建立陣列

陣列在Java裡是比較特殊的一種型別,它可以賦值給一個Object Reference。下面我們看一看利用反射建立陣列的例子:

public static void testArray() throws ClassNotFoundException {
    Class<?> cls = Class.forName("java.lang.String");
    Object array = Array.newInstance(cls,10);
    //往陣列裡新增內容
    Array.set(array,0,"Java");
    Array.set(array,1,"C++");
    Array.set(array,2,"JavaScript");
    Array.set(array,3,"Python");
    Array.set(array,4,"Kotlin");
    //獲取某一項的內容
    System.out.println(Array.get(array,3));
    }
複製程式碼

其中的Array類為java.lang.reflect.Array類。我們通過Array.newInstance()建立陣列物件。第一個引數是陣列的型別,第二個引數是陣列的長度。

3、反射的作用

反射最重要的用途就是開發各種通用框架。
很多框架(如Spring)都是配置化的(比如通過XML檔案配置JavaBean,Action之類的),為了保證框架的通用性,它們可能需要根據配置檔案載入不同的物件或類,呼叫不同的方法,這個時候就必須用到反射--執行時動態載入需要載入的物件。

相關文章