不學無數——初識反射
反射:執行時的類資訊
執行時型別資訊使得你可以在程式執行時發現和使用型別資訊
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
類庫一起對反射的概念進行了支援,該類庫包含了Field
,Method
以及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