什麼是 java 的反射?
說到反射,寫這篇文章時,我突然想到了人的」反省「,反省是什麼?吾一日三省吾身,一般就是反思自身,今天做了哪些對或錯的事情。
java 的反射,我覺得有同樣的思想。當然 java 反射要「反思」的是 java 程式在執行時類自己的資訊,它獲取的資訊就是它自身類的詳細資訊。
類的哪些詳細資訊呢?比如類或物件的成員變數、方法等。然後可以對這些資訊加以修改,從而調整 java 的執行邏輯。
java 反射 API 提供了非常豐富的工具集,反射 API 能夠獲取物件的變數,方法等成員,從而可以動態的操縱 java 程式碼程式。文章後面會介紹這些反射 API(反射相關的類)的一些用法。
為什麼反射能得到 java 程式執行時類的資訊呢?這就要從 java 的虛擬機器器 jvm 說起。
Java虛擬機器器(Java Virtual Machine):用於執行編譯後的 java 程式的虛擬容器。jvm 可以跨作業系統使用。
jvm 內部結構分為 3 部分:類載入器classload子系統、執行時資料區、執行引擎。
以 .java
結尾的檔案是不能直接在 jvm 上執行,它必須通過 javac 編譯為以 .class
為字尾結尾的位元組碼檔案才能執行。
java 檔案被編譯為 .class 的檔案後,java 檔案中各種物件的資訊就確定下來了,存在於 .class 檔案裡。通過 java 的反射就可以獲取裡面的資訊。
在上一小節簡單瞭解了檔案載入內容,就是 java 檔案經過編譯後變成 .class 檔案,類的各種資訊就儲存在 .class 檔案中了,所以反射才能獲取到類的各種資訊。
java 程式碼編譯為位元組碼的 .class 類檔案,那 .class 檔案裡都有什麼格式是什麼?
class 檔案結構採用類似 c 語言的結構體來儲存資料。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
它由 2 部分組成:無符號的數和表。所有的表都習慣以 _info
結尾。
無符號的數屬於基本的資料型別,一 u1,u2,u4,u8 分別來表示 1 個位元組,2 個位元組,4 個位元組和 8 個位元組的無符號數,無符號數可以用來表示數位、索引參照、數量值。
表用於描述有層次關係的複合結構的資料,整個 class 檔案本質就是一張表。
類的資訊都儲存在 .class 檔案裡,當把 .class 檔案讀入記憶體時,就會為之建立一個 Class 物件。
這裡又涉及到虛擬機器器載入類的機制。周志明的《深入理解java虛擬機器器》裡有一節講類載入的內容,上面 class 檔案結構也來自本書,可以好好看一看此書。
簡單說:java 虛擬機器器把描述類的資料 class 檔案載入到記憶體,並對資料進行效驗、轉換解析和初始化,最終形成可以被虛擬機器器直接使用的型別,這就是虛擬機器器的類載入機制。
在 java 語言裡,類的載入和連線過程都是在程式執行期間完成的,雖然有效能開銷,但是為 java 應用程式提供了高度的靈活性。比如反射就是發生在 java 執行時完成的。
在類載入階段,虛擬機器器會在 java 堆中生成一個代表這個類的 java.lang.Class 物件,通過這個 Class 物件就可以存取到 jvm 中的這個類。
Class 類與 class 是不同,Class 是實實在在存在於 java.lang.Class 包中的一個類。
反射:獲取 Class 類物件及其類內部成員資訊(屬性, 方法, 建構函式等)以及控制範例物件的能力
在 java 中,要使用反射功能,主要用到下面 2 個類:
java.lang.Class
java.lang.reflect
- java.lang.reflect.Constructor, 獲取構造方法,Class 物件所表示類的構造方法
- Java.lang.reflect.Field, 欄位成員,Class 物件所表示的類的成員變數,通過它可以動態修改成員變數值,包含 private
- Java.lang.reflect.Method,方法成員,Class 物件所表示的類的方法成員,通過它可以動態呼叫物件的方法
- Java.lang.reflect.Modifier,對類和成員存取修飾符進行解碼
獲取 class 類物件的方法,主要有 3 種:
根據 forName 靜態方法:
Class.forName(類的全限定名)
。該方法是傳入一個字串引數,該引數是某個類的全限定完整包名。根據類名直接獲取:
類名.class
。呼叫某個類的 class 屬性獲取 Class 物件,比如 Student.class 返回 Student 類對應的 Class 物件。根據物件獲取:
物件.getClass()
。該方法是 java.lang.Object 類中的一個方法,所有 java 物件都可以呼叫該方法。
(三種獲取 Class 物件的方法)
先舉一個小例子來看看這 3 種獲取 Class 物件的用法。
java v1.8
第一步:用 IDEA 新建 maven 專案,名字叫 JavaBasicDemos
目錄如下:
JavabasicDemos
|-.idea
|-src
|-main
| |-java
| | |-org.example
| | |-Main.java
| |-resource
|-test
把上面的 org.example 改成 org.basicdemo。然後在 org.basicdemo 下新建 reflect/reflectdemo1.java,student.java 檔案,目錄如下:
第二步:編寫 student.java 程式碼
package org.basicdemo.reflect;
public class student {
private String name = "Tom";
private int age;
public student(){}
public student(String name, int age) {
this.name = name;
this.age = age;
}
private void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
第三步:編寫 reflectdemo1.java
package org.basicdemo.reflect;
public class reflectdemo1 {
public static void main(String[] args) {
try {
Class<?> stuclz1 = Class.forName("org.basicdemo.reflect.student");
System.out.println("Class.forName: " + stuclz1);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
student stu = new student();
Class stuclz2 = stu.getClass();
System.out.println("物件.getClass(): " + stuclz2);
Class stuclz3 = student.class;
System.out.println("類名.class: " + stuclz3);
}
}
點選IDEA上執行程式的綠色三角形按鈕,輸出如下:
Class.forName: class org.basicdemo.reflect.student
物件.getClass(): class org.basicdemo.reflect.student
類名.class: class org.basicdemo.reflect.student
此demo完整詳細程式碼在 github 上:reflect demo
其他一些常用寫法:
Class<?> clazz = Class.forName("Student");
System.out.println(clazz);
// or
Class cls = Class.forName("Student");
System.out.println(cls);
// 以前最常用反射獲取jdbc
Class.forName("com.mysql.jdbc.Driver.class").
Class 類的其它一些方法:
更多方法請檢視 Class 的檔案:https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html。
Class<Student> clz = Studnt.class
Student stu = clz.newInstance()
// 或者
Student stu = Student.class.newInstance()
//==========
Class<?> c = String.class;
Object str = c.newInstance();
通過 Class 物件獲取 Constructor,再呼叫 Constructor 物件的 newInstance() 方法來建立物件
// 獲取構造方法 Integer(int)
Construct construct1 = Integer.class.getConstructor(int.class)
Integer n1 = construct1.newInstance(123) // 呼叫構造方法
System.out.println(n1);
// 獲取構造方法 Integer(String)
Constructor construct2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) construct2.newInstance("567");
System.out.println(n2);
兩種方法區別:
newInstance() 的侷限是,它只能呼叫該類的 public 無引數構造方法。如果構造方法帶有引數,或者不是 public,就無法直接通過Class.newInstance() 來呼叫。
為了呼叫任意的構造方法,反射 API 提供了 Constructor 物件,它包含一個構造方法的所有資訊,可以建立一個範例。
通過反射來獲取構造方法,然後使用。
java 反射裡的 Constructor 類,Class Constructor
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Constructor.html。
java.lang.Object
java.lang.reflect.AccessibleObject
java.lang.reflect.Executable
java.lang.reflect.Constructor<T>
通過 Class 類來獲取類的構造方法主要有 4 個,分別為獲取單個構造方法和獲取多個構造方法。
方法 | 使用說明 |
---|---|
public Constructor<?>[] getConstructors() | 返回所有public許可權的建構函式的物件陣列。 |
public Constructor<?>[] getDeclaredConstructors() | 獲取所有建構函式方法,包括所有許可權public,private,protected,default許可權。 |
public Constructor |
獲取單個public許可權的構造方法。 |
public Constructor |
獲取單個構造方法,包括所有許可權public,private,protected,default許可權。 |
newInstance(Object... initargs) ,使用此 Constructor 物件表示的構造方法,用它來建立類的新範例,並用指定的初始化引數初始化該範例。上面也有講到過該方法使用。
寫一個 demo 例子:
此demo完整詳細程式碼在 github 上:github-reflect construct,下面說說編寫程式碼步驟。
第一步:新建一個 reflectconstructor 包,然後新建 2 個 java 檔案,reflectconstructdemo1.java 和 student.java,如下圖
第二步:把上一小節的 student.java 程式碼複製過來,然後增加幾個建構函式
student(String name) {
System.out.println("(student)private construct-age: "+age);
}
public student(){
System.out.println("no args");
}
public student(String name, int age) {
this.name = name;
this.age = age;
}
private student(int age) {
System.out.println("private construct-age: "+age);
}
第三步:在 reflectconstructdemo1.java 裡編寫獲取建構函式方法
首先獲取 Class 物件:
Class stuclz = Class.forName("org.basicdemo.reflectconstructor.student");
獲取所有公有建構函式方法 getConstructors()
// 獲取所有公有(public)構造方法
System.out.println("===========獲取所有公有構造方法=========");
Constructor[] consarr = stuclz.getConstructors();
for(Constructor c : consarr) {
System.out.println(c);
}
獲取所有的建構函式方法 getDeclaredConstructors()
// 獲取所有(public,protected,private,default)的構造方法
System.out.println("===========獲取所有的構造方法=========");
Constructor[] consall = stuclz.getDeclaredConstructors();
for(Constructor c : consall) {
System.out.println(c);
}
獲取單個建構函式方法(公有、無參的方法)
// 獲取單個構造方法,公有無參的構造方法
System.out.println("===========獲取單個公有、無引數的構造方法=========");
try {
Constructor con = stuclz.getConstructor(null);
System.out.println("con: " + con);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
獲取單個私有private構造方法
System.out.println("===========獲取單個私有private構造方法=========");
Constructor con = stuclz.getDeclaredConstructor(int.class);
System.out.println(con);
// 呼叫設定能存取
con.setAccessible(true); // 因為是私有,所以必須設定能存取
// 建立 student 物件
student stu = (student) con.newInstance(12);
stu.setAge(13);
System.out.println("age: "+stu.getAge());
此demo完整詳細程式碼在 github 上:github-reflect constructor
通過 Class 類提供的方法來獲取成員欄位資訊,主要方法有 4 種,也分為獲取單個和多個成員,是公有還是私有,還是所有許可權都能獲取。
方法 | 使用說明 |
---|---|
public Field getField(String name) | 根據名字獲取單個public許可權的欄位 |
public Field getDeclaredField(String name) | 根據名字獲取某個欄位,欄位許可權可以是所有,包括private |
public Field[] getFields() | 獲取所有public許可權欄位 |
public Field[] getDeclaredFields() | 獲取所有欄位,欄位許可權可以是所有,包括private |
第一步:建立一個 reflectfield 的包,然後新建 2 個 java 檔案,reflectfielddemo1.java 和 student.java,如下圖
第二步:把上一小節 reflectconstructor 包裡的 student 類程式碼複製到這裡的 student.java 裡,然後新增幾個欄位
public String address;
public int grade;
protected String email;
String phone;
第三步:編寫獲取欄位的方法
獲取 Class 物件
// 獲取 Class 物件
Class stuClz = Class.forName("org.basicdemo.reflectfield.student");
獲取所有欄位的方法
// 獲取所有 public 許可權的欄位
System.out.println("==========獲取所有 public 許可權的欄位===========");
Field[] fieldArr = stuClz.getFields();
for(Field f : fieldArr) {
System.out.println(f+" - ("+f.getDeclaringClass() +") - ("+f.getName()+":"+f.getType()+")");
}
// 獲取所有許可權的欄位,包括private
System.out.println("==========獲取所有許可權的欄位,包括private===========");
Field[] fieldsArr = stuClz.getDeclaredFields();
for(Field f : fieldsArr) {
System.out.println(f);
}
獲取單個欄位方法
// 根據名字獲取單個public欄位
System.out.println("===========根據名字獲取public欄位============");
Field addressField = stuClz.getField("address");
System.out.println(addressField);
// 根據反射來設定下這個欄位
Object obj = stuClz.getConstructor().newInstance();
// 用 set 方法來設定欄位的值
addressField.set(obj, "setTestValue");
// 列印設定的值
student stu = (student) obj;
System.out.println("print address value: " + stu.address);
// 根據名字獲取某個欄位,欄位許可權包括所有,也包括private
System.out.println("=========根據名字獲取某個欄位,欄位許可權包括所有,也包括private=======");
// 來獲取一個 private 欄位
Field nameField = stuClz.getDeclaredField("name");
System.out.println(nameField);
// 沒有設定前的name值
System.out.println("name value before setting: "+stu.getName());
// 來設定值
nameField.setAccessible(true); // 因為是private,所以先要設定可存取。相當於開啟一個開關,原本是不可以寫的。
nameField.set(obj, "jimmy");
System.out.println("name value after setting: " + stu.getName());
IDEA 上程式碼執行輸出:
==========獲取所有 public 許可權的欄位===========
public java.lang.String org.basicdemo.reflectfield.student.address - (class org.basicdemo.reflectfield.student) - (address:class java.lang.String)
public int org.basicdemo.reflectfield.student.grade - (class org.basicdemo.reflectfield.student) - (grade:int)
==========獲取所有許可權的欄位,包括private===========
private java.lang.String org.basicdemo.reflectfield.student.name
private int org.basicdemo.reflectfield.student.age
public java.lang.String org.basicdemo.reflectfield.student.address
public int org.basicdemo.reflectfield.student.grade
protected java.lang.String org.basicdemo.reflectfield.student.email
java.lang.String org.basicdemo.reflectfield.student.phone
===========根據名字獲取public欄位============
public java.lang.String org.basicdemo.reflectfield.student.address
no args
print address value: setTestValue
=========根據名字獲取某個欄位,欄位許可權包括所有,也包括private=======
private java.lang.String org.basicdemo.reflectfield.student.name
name value before setting: Tom
print name value after setting: jimmy
方法名 | 使用說明 |
---|---|
Object get(Object obj) | 返回指定物件上欄位值 |
void set(Object obj, Object value) | 指定物件上為field欄位設定新值 |
Class<?> getType() | field欄位表示的宣告的欄位型別 |
Class<?> getDeclaringClass() | field欄位所在類的Class物件 |
String getName() | 返回欄位名稱 |
void setAccessible(boolean flag) | 物件的 accessible 標誌設定,可以設定欄位的存取性。比如設定為true表示其可寫 |
更多方法可以檢視 Field 類的 API: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html
此demo完整詳細程式碼在 github 上:github-reflect field
通過 Class 類獲取 Method 物件的方法,與上面獲取欄位field方法相似,也有 4 種,分為獲取單個和獲取多個方法。
方法 | 使用說明 |
---|---|
public Method[] getMethods() | 獲取所有public的方法,包含父類別的方法 |
public Method getDeclaredMethod(String name, Class<?> ...parameterTypes) | 獲取所有的方法,包括private |
public Method getMethod(String name, Class<?>... parameterTypes) | 獲取單個public的方法 |
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 根據名字獲取所有的方法 |
第一步:與上一小節filed一樣,先新建一個 reflectmethod 包,然後在裡面新建 2 個 java 檔案,reflectmethoddemo1.java 和 student.java,如下圖:
第二步:把上一小節的 student.java 程式碼複製到這裡
並增加一個基礎 student.java 的class TomStudent,
class TomStudent extends student {
private void printName() {
System.out.println("a student name: Tom");
}
public int getTomAge() {
return 12;
}
public void setTomeAge(int age) {
this.setAge(age);
}
}
第三步:編寫獲取方法的程式碼
獲取多個的方法
// 獲取所有public method方法
System.out.println("=============獲取所有public method方法,包括繼承父類別的===============");
Method[] methodArr = stuClz.getMethods();
for(Method m:methodArr) {
System.out.println(m); // 不僅列印出了 TomStudent 所有 public 方法,它繼承的方法也列印出來
}
獲取單個的方法:
// 根據引數獲取public的方法,包含繼承自父類別的方法
System.out.println("=======根據引數獲取public的方法,包含繼承自父類別的方法======");
Method method = stuClz.getMethod("setAge", int.class);
System.out.println(method);
詳細的demo程式碼檢視:github reflect method
// 根據引數獲取public的方法,包含繼承自父類別的方法
System.out.println("=======根據引數獲取public的方法,包含繼承自父類別的方法======");
Method method = stuClz.getMethod("setAge", int.class);
System.out.println(method);
// 反射呼叫方法
Object obj = stuClz.newInstance();
method.invoke(obj, 12);
student stu = (student)obj;
System.out.println(stu.getAge());
如果是呼叫private方法,一定要設定 setAccessible(true)
。
說明:上面所有的程式碼以 github 上的為準:reflect demo系列
等用途。