不學無數——初識反射

weixin_33686714發表於2018-09-03

反射:執行時的類資訊

執行時型別資訊使得你可以在程式執行時發現和使用型別資訊

1. Class物件

通過Class物件可以在執行時發現一個物件完整的類繼承結構

類是程式的一部分,每一個類都會有一個Class物件。換句話說既每編寫一個新的類,就會產生一個Class物件。而這些Class物件資訊是儲存在我們用javac 類名.java 進行編譯時產生的.class檔案中的。為了生成這個物件,執行這個程式的java虛擬機器(JVM)會使用類載入器進行載入

1.1 什麼是類載入器

Java類載入器(Java Classloader)是Java執行時環境(Java Runtime Environment)的一部分,負責動態載入Java類到Java虛擬機器的記憶體空間中

即類載入器是Java虛擬機器將描述類的資料,例如類的各種方法,構造引數之類的資訊從Class檔案載入到記憶體中去,並且對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別。

所有的類都是在對其進行第一次使用的時候,動態載入到JVM中的,即和編譯時需要進行連線工作的語言不通,在java中,型別的載入、連線和初始化都是在程式執行期間完成的。

類載入器在進行載入的時候會首先檢查這個類的Class物件是否已經進行了載入,如果沒有載入,那麼預設的類載入器就會根據類名進行查詢.class檔案。通過下面例子,我們可以清楚看出類是在第一次被使用的時候才會載入。

static靜態程式碼塊的載入是在類載入的時候進行的

class Tom{
    static {
        System.out.println("Loading Tom");
    }
}

class Jerry{
    static {
        System.out.println("Loading Jerry");
    }
}

class Mary{
    static {
        System.out.println("Loading Mary");
    }
}
public class Demo4 {
    public static void main(String[] args) {
        System.out.println("Inside Main");
        new Tom();
        System.out.println("After Loading Tom");
        new Mary();
        System.out.println("After Loading Mary");
        new Jerry();
    }
}

複製程式碼

輸出結果如下:

Inside Main
Loading Tom
After Loading Tom
Loading Mary
After Loading Mary
Loading Jerry

複製程式碼

類載入器在載入類的過程中會分為三個階段

  • 載入:載入.class檔案的位元組碼檔案
  • 連線:為類分配靜態域,併為變數分配初始值
  • 初始化:會真正的執行類中定義的java程式程式碼,既初始化靜態域中的方法和變數

同一個類載入器下,一個型別只會初始化一次。

1.2 建立Class物件

  • 根據物件的引用.getClass()方法獲取
Tom tom = new Tom();
Class class = tom.getClass();
複製程式碼
  • 根據類名.class獲取
Class class = Tom.class;

複製程式碼
  • 根據Class中的靜態方法Class.forName,其中的引數必須為帶包名的類路徑
 Class c = Class.forName("Tom");

複製程式碼

通常在反射中建立Class物件時使用的第三種方法,因為第一種已經有這個物件了,幹嘛還需要反射,第二種的話會有侷限性,需要匯入所要建立物件的包路徑。

2. 使用反射

我們在上面獲取到Class物件,而我們拿到了Class物件以後就能對其進行操作

2.1 構造方法

Class類和javalang.reflect類庫一起對反射的概念進行了支援,該類庫包含了FieldMethod以及Constructor類(每個類都實現了Member介面).這些型別的物件是由JVM在執行時建立的,用以表示未知類裡所對應的成員資訊.這樣就可以用Constructor建立新的物件。下面演示一下通過反射獲取構造方法。

2.1.1 獲取公有構造方法

public class A {
    private String a;
    private String b;
    public String c;

    public A(String a, String b) {
        this.a = a;
        this.b = b;
    }

    private A(String a){
        this.a=a;
    }
    public A(){}
   ——————————get.set方法省略
}
複製程式碼
Class a=Class.forName("Practice.Day05.A");
Constructor[] constructors = a.getConstructors();
for (Constructor constructor:constructors){
    System.out.println(constructor);
}

複製程式碼

此時我們發現列印出來的資訊如下:

public Practice.Day05.A()
public Practice.Day05.A(java.lang.String,java.lang.String)
複製程式碼

2.1.2 獲取所有構造方法

我們發現沒有私有的建構函式,因為getConstructors()方法獲得是public的建構函式,而getDeclaredFields()方法獲得是包括public, protected, default,private的建構函式,此時如果我們將程式碼改成如下:

Class a=Class.forName("Practice.Day05.A");
Constructor[] constructors = a.getDeclaredConstructors();
for (Constructor constructor:constructors){
    System.out.println(constructor);

}
複製程式碼

我們發現列印的引數就會多了一個private的建構函式

public Practice.Day05.A()
private Practice.Day05.A(java.lang.String)
public Practice.Day05.A(java.lang.String,java.lang.String)
複製程式碼

2.1.3 獲得具體型別的構造方法

我們在上面都是獲得了一個構造方法的陣列,如果想要獲得具體的構造方法的話,那麼可以通過傳入構造方法的入參的型別,可以獲得這個構造方法,具體例子如下:

Class a=Class.forName("Practice.Day05.A");
Constructor constructor = a.getConstructor(null);
Constructor stringConstructor = a.getConstructor(String.class,String.class);
Constructor stringPrivateConstructor=a.getDeclaredConstructor(String.class);
System.out.println(constructor);
System.out.println(stringConstructor);
System.out.println(stringPrivateConstructor);

複製程式碼

列印的資訊如下:

public Practice.Day05.A()
public Practice.Day05.A(java.lang.String,java.lang.String)
private Practice.Day05.A(java.lang.String)
複製程式碼

####2.1.4 通過構造方法例項化類

我們獲得了Constructor物件以後,可以通過其中的newInstance方法進行例項化物件。例子如下:

Class a=Class.forName("Practice.Day05.A");
Constructor constructor = a.getConstructor(null);
Constructor stringConstructor = a.getConstructor(String.class,String.class);
A nullA= (A) constructor.newInstance();
A stringA= (A) stringConstructor.newInstance("BuXueWuShu","BuXueWuShu");
nullA.setA("BuXueWuShu");
System.out.println("nullA:"+nullA.getA());
System.out.println("stringA:"+stringA.getA());

複製程式碼

列印資訊如下:

nullA:BuXueWuShu
stringA:BuXueWuShu
複製程式碼

2.2 成員變數

還是上面的實體類的例子,其中有私有的兩個變數是a和b,私有的變數是c。

2.2.1 獲取成員變數

如果類中有屬性的話,兩個方法都是返回一個Field陣列.其中getDeclaredFields()方法返回的是所有引數包括public, protected, default,private,而getFields()只返回public的引數。例如如下

Class a=Class.forName("Practice.Day05.A");
Field[] allDeclaredFields = a.getDeclaredFields();--獲得所有成員變數
Field[] fields = a.getFields();--只獲得public的成員變數
for (Field field:allDeclaredFields){
    System.out.println(field);
}
System.out.println("-----------------------");
for (Field field:fields){
    System.out.println(field);
}

複製程式碼

列印的引數如下:

private java.lang.String Practice.Day05.A.a
private java.lang.String Practice.Day05.A.b
public java.lang.String Practice.Day05.A.c
-----------------------
public java.lang.String Practice.Day05.A.c
複製程式碼

2.2.2 獲得指定成員變數

獲得指定的成員變數其實和上面的獲取指定的構造方法是一樣的。舉例如下:

Class a=Class.forName("Practice.Day05.A");
Field c1 = a.getField("c");
Field a1 = a.getDeclaredField("a");
System.out.println(c1);
System.out.println("---------------------");
System.out.println(a1);

複製程式碼

列印引數如下:

public java.lang.String Practice.Day05.A.c
---------------------
private java.lang.String Practice.Day05.A.a

複製程式碼

2.2.3 為成員變數賦值

獲得了Field的物件後,可以呼叫get()set()方法對某個物件中的屬性進行取值和賦值的操作。例子如下

Class a=Class.forName("Practice.Day05.A");
Constructor nullClass=a.getDeclaredConstructor(null);
A nullA= (A) nullClass.newInstance();--獲得A的例項化物件
Field a1 = a.getDeclaredField("a");
a1.setAccessible(true);--變數a是private的,所以需要解除私有限定
a1.set(nullA,"BuXueWuShu");--為nullA物件中的變數a進行賦值操作
System.out.println("nullA="+a1.get(nullA));--取出nullA物件中的變數a

複製程式碼

列印資訊如下:

nullA=BuXueWuShu

複製程式碼

注意在對私有的成員變數進行賦值操作時,要解除私有限定,呼叫setAccessible()方法,賦值為true

2.3 成員方法

2.3.1 獲取成員方法

通過獲得的Class物件,呼叫它的getDeclaredMethods()getMethods()方法可以獲得類中的方法的資訊。例如有以下的一個類。


public class TestMethod {

    private String playName;

    public void show(String playName){
        System.out.println("I Love "+playName);
    }

    private String returnPlayName(){
        return playName;
    }
}

複製程式碼

做如下的呼叫:

Class a=Class.forName("Practice.Day05.TestMethod");
Method[] allDeclaredMethods = a.getDeclaredMethods();
Method[] methods = a.getMethods();
for (Method method:allDeclaredMethods){
    System.out.println(method);
}
System.out.println("-----------------");
for (Method method:methods){
    System.out.println(method);
}

複製程式碼

可以發現列印如下的資訊:

public void Practice.Day05.TestMethod.show(java.lang.String)
private java.lang.String Practice.Day05.TestMethod.returnPlayName()
-----------------
public void Practice.Day05.TestMethod.show(java.lang.String)
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 final void java.lang.Object.wait() 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()

複製程式碼

getDeclaredMethods()方法會獲得自身和實現的介面中所有的方法,但是不會獲得繼承來的方法,getMethods()方法會獲得所有的無論是實現的介面還是繼承來的public的方法

2.3.2 獲得指定的方法

通過方法名和方法中的引數型別就可以獲得指定的方法

Class a=Class.forName("Practice.Day05.TestMethod");
Method show = a.getDeclaredMethod("show", String.class);
System.out.println(show);

複製程式碼

列印資訊如下:

public void Practice.Day05.TestMethod.show(java.lang.String)

複製程式碼

2.3.3 使用方法

可以通過呼叫Method物件的invoke()方法進行呼叫方法。例子如下:

Class a=Class.forName("Practice.Day05.TestMethod");
Constructor nullClass=a.getDeclaredConstructor(null);
TestMethod nullTestMethod= (TestMethod) nullClass.newInstance();
Method show = a.getDeclaredMethod("show",String.class);
show.invoke(nullTestMethod,"BasketBall");

複製程式碼

列印引數如下:

I Love BasketBall

複製程式碼

如果呼叫的是private的方法,那麼在使用invoke()方法之前要先解除私有限定,即呼叫setAccessible()方法,賦值為true

相關文章