對于一般的開發(fā)者,很少需要直接使用Java反射機(jī)制來完成功能開發(fā),但是反射是很多框架譬如 Spring摔吏, Mybatis 實現(xiàn)的核心韧拒,反射雖小,能量卻很大。
本文主要介紹反射相關(guān)的概念以及API的使用,關(guān)于反射的應(yīng)用將在下一篇文章中介紹
反射的介紹
反射(Reflection) 是 Java 在運(yùn)行時(Run time)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力贝咙,它允許運(yùn)行中的 Java 程序獲取自身的信息,并且可以操作類或?qū)ο蟮膬?nèi)部屬性拂募。
Class 類介紹:Java虛擬機(jī)為每個類型管理一個Class對象庭猩,包含了與類有關(guān)的信息,當(dāng)通過 javac 編譯Java類文件時陈症,生成的同名 .class 文件保存著該類的 Class 對象蔼水,JVM 加載一個類即是加載該 .class 文件。
Class
和 java.lang.reflect
一起對反射提供了支持爬凑,java.lang.reflect 包中最常用的幾個類的關(guān)系如下:
其中最主要的三個類 Field
徙缴、Method
和 Constructor
分別用于描述類的域、方法和構(gòu)造器嘁信,它們有一個共同的父類 AccessibleObject
于样,它提供了訪問控制檢查的功能。
- Field :描述類的域(屬性)潘靖,可以使用 get() 和 set() 方法讀取和修改 Field 對象關(guān)聯(lián)的字段穿剖;
- Method :描述類的方法,可以使用 invoke() 方法調(diào)用與 Method 對象關(guān)聯(lián)的方法卦溢;
- Constructor :描述類的構(gòu)造器糊余,可以用 Constructor 創(chuàng)建新的對象秀又。
下面將通過幾個程序來學(xué)習(xí)Java反射機(jī)制。
準(zhǔn)備兩個類用于實驗
我們特別定義兩個類贬芥,Person和Employee吐辙,其中Employee繼承自Person,且各自都有一個private蘸劈,protected昏苏,public修飾的域(屬性),Employee還有private威沫,public修飾的方法
public class Person {
public String name; // 姓名 公有
protected String age; // 年齡 保護(hù)
private String hobby; // 愛好 私有
public Person(String name, String age, String hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
public String getHobby() {
return hobby;
}
}
public class Employee extends Person {
public static Integer totalNum = 0; // 員工數(shù)
public int empNo; // 員工編號 公有
protected String position; // 職位 保護(hù)
private int salary; // 工資 私有
public void sayHello() {
System.out.println(String.format("Hello, 我是 %s, 今年 %s 歲, 愛好是%s, 我目前的工作是%s, 月入%s元\n", name, age, getHobby(), position, salary));
}
private void work() {
System.out.println(String.format("My name is %s, 工作中勿擾.", name));
}
public Employee(String name, String age, String hobby, int empNo, String position, int salary) {
super(name, age, hobby);
this.empNo = empNo;
this.position = position;
this.salary = salary;
Employee.totalNum++;
}
}
獲取 Class 對象
獲取 Class 對象的方式有三種:使用 Class 類的 forName 靜態(tài)方法贤惯;直接獲取某一個對象的 class;調(diào)用某個對象的 getClass() 方法
public class ClassTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class c1 = Class.forName("reflect.Employee"); // 第1種棒掠,forName 方式獲取Class對象
Class c2 = Employee.class; // 第2種孵构,直接通過類獲取Class對象
Employee employee = new Employee("小明", "18", "寫代碼", 1, "Java攻城獅", 100000);
Class c3 = employee.getClass(); // 第3種,通過調(diào)用對象的getClass()方法獲取Class對象
if (c1 == c2 && c1 == c3) { // 可以通過 == 比較Class對象是否為同一個對象
System.out.println("c1烟很、c2颈墅、c3 為同一個對象");
System.out.println(c1); // class reflect.Employee
}
}
}
通過反射來創(chuàng)建實例
通過反射來生成對象主要有兩種方式
- 使用Class對象的newInstance()方法來創(chuàng)建Class對象對應(yīng)類的實例
- 先通過Class對象獲取指定的Constructor對象,再調(diào)用Constructor對象的newInstance()方法來創(chuàng)建實例
public class NewInstanceTest {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class c = Date.class;
Date date1 = (Date) c.newInstance(); // 第1種方式:使用Class對象的newInstance()方法來創(chuàng)建Class對象對應(yīng)類的實例
System.out.println(date1); // Wed Dec 19 22:57:16 CST 2018
long timestamp =date1.getTime();
Constructor constructor = c.getConstructor(long.class);
Date date2 = (Date)constructor.newInstance(timestamp); // 第2種方式:先通過Class對象獲取指定的Constructor對象溯职,再調(diào)用Constructor對象的newInstance()方法來創(chuàng)建實例
System.out.println(date2); // Wed Dec 19 22:57:16 CST 2018
}
}
獲取類的全部信息
上面我們定義了兩個類精盅,現(xiàn)在有個需求:獲取Employee的類名帽哑,構(gòu)造器簽名谜酒,所有的方法,所有的域(屬性)和值妻枕,然后打印出來僻族。該通過什么方式來實現(xiàn)呢?
沒錯屡谐,猜對了述么,就是通過反射來獲取這些類的信息,在上面介紹中我們知道JVM虛擬機(jī)為每個類型管理一個Class對象愕掏,
為了完成我們的需求度秘,我們需要知道一些API如下:
獲取類信息的部分API
String getName()
獲取這個Class的類名
Constructor[] getDeclaredConstructors()
返回這個類的所有構(gòu)造器的對象數(shù)組,包含保護(hù)和私有的構(gòu)造器饵撑;相近的方法 getConstructors() 則返回這個類的所有公有構(gòu)造器的對象數(shù)組剑梳,不包含保護(hù)和私有的構(gòu)造器
Method[] getDeclaredMethods()
返回這個類或接口的所有方法,包括保護(hù)和私有的方法滑潘,不包括超類的方法垢乙;相近的方法 getMethods() 則返回這個類及其超類的公有方法的對象數(shù)組,不含保護(hù)和私有的方法
Field[] getDeclaredFields()
返回這個類的所有域的對象數(shù)組语卤,包括保護(hù)域和私有域追逮,不包括超類的域酪刀;還有一個相近的API getFields()
,返回這個類及其超類的公有域的對象數(shù)組钮孵,不含保護(hù)域和私有域
int getModifiers()
返回一個用于描述Field骂倘、Method和Constructor的修飾符的整形數(shù)值,該數(shù)值代表的含義可通過Modifier這個類分析
Modifier 類
它提供了有關(guān)Field巴席、Method和Constructor等的訪問修飾符的信息稠茂,主要的方法有:toString(int modifiers)返回整形數(shù)值modifiers代表的修飾符的字符串;isAbstract是否被abstract修飾情妖;isVolatile是否被volatile修飾睬关;isPrivate是否為private;isProtected是否為protected毡证;isPublic是否為public电爹;isStatic是否為static修飾;等等料睛,見名知義
打印類信息程序
public class ReflectionTest {
public static void main(String[] args) throws ClassNotFoundException {
String name;
if (args.length > 0) {
name = args[0];
} else {
Scanner in = new Scanner(System.in);
System.out.println("輸入一個類名(e.g. java.util.Date):"); // reflect.Employee
name = in.next();
}
try {
Class cl = Class.forName(name);
Class superCl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print("class " + name);
if (superCl != null && superCl != Object.class) {
System.out.print(" extends " + superCl.getName());
}
System.out.println("\n{");
printConstructors(cl); // 打印構(gòu)造方法
System.out.println();
printMethods(cl); // 打印方法
System.out.println();
printFields(cl); // 打印屬性
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
/**
* 打印Class對象的所有構(gòu)造方法
*/
public static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(name + "(");
// 打印構(gòu)造參數(shù)
Class[] paramTypes = c.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* 打印Class的所有方法
*/
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
//Method[] methods = cl.getMethods();
for (Method m : methods) {
Class retType = m.getReturnType(); // 返回類型
System.out.print(" ");
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " " + m.getName() + "(");
Class[] paramTypes = m.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* 打印Class的所有屬性
*/
public static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f: fields) {
Class type = f.getType();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length()> 0) {
System.out.print(modifiers + " ");
}
System.out.println(type.getName() + " " + f.getName() + ";");
}
}
}
運(yùn)行程序丐箩,然后在控制臺輸入一個我們想分析的類的全名,譬如 reflect.Employee恤煞,可得到下面的輸出
輸入一個類名(e.g. java.util.Date):
reflect.Employee
public class reflect.Employee extends reflect.Person
{
private reflect.Employee(java.lang.String, java.lang.String, java.lang.String);
public reflect.Employee(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String, int);
public static void main([Ljava.lang.String;);
public void sayHello();
private void work();
public static java.lang.Integer totalNum;
public int empNo;
protected java.lang.String position;
private int salary;
}
上面的輸出中我們得到的類的構(gòu)造器屎勘,所有方法和所有的域(屬性),包括修飾符居扒,名稱和參數(shù)類型都是準(zhǔn)確的概漱,看來反射機(jī)制能完成我們的需求。
小結(jié)一下喜喂,我們通過 getDeclaredConstructors() 獲取構(gòu)造器信息瓤摧,通過 getDeclaredMethods() 獲得方法信息,通過 getDeclaredFields() 獲得域信息玉吁,再通過 getModifiers() 和 Modifier類 獲得修飾符信息照弥,匯總起來就得到了整個類的類信息。
運(yùn)行時查看對象數(shù)據(jù)域的實際內(nèi)容
上面我們已經(jīng)獲取到了類的信息进副,現(xiàn)在又有一個需求:在運(yùn)行時查看對象的數(shù)據(jù)域的實際值这揣。這個場景就像我們通過IDEA調(diào)試程序,設(shè)置斷點攔截到程序后影斑,查看某個對象的屬性的值给赞。
我們知道java反射機(jī)制提供了查看類信息的API,那么它應(yīng)該也提供了查看Field域?qū)嶋H值和設(shè)置Field域?qū)嶋H值的API鸥昏,沒錯塞俱,猜對了,確實有相關(guān)的API吏垮,但是有個疑問障涯,有一些屬性是private修飾的私有域罐旗,這種是否也能直接查看和設(shè)置呢?看完下面的API即可知道答案
運(yùn)行時查看對象數(shù)據(jù)域?qū)嶋H內(nèi)容的相關(guān)API
Class<?> getComponentType()
返回數(shù)組類里組件類型的 Class唯蝶,如果不是數(shù)組類則返回null
boolean isArray()
返回這個類是否為數(shù)組九秀,同類型的API還有 isAnnotation、isAsciiDigit粘我、isEnum鼓蜒、isInstance、isInterface征字、isLocalClass都弹、isPrimitive 等
int Array.getLength(obj)
返回數(shù)組對象obj的長度
Object Array.get(obj, i)
獲取數(shù)組對象下標(biāo)為i的元素
boolean isPrimitive()
返回這個類是否為8種基本類型之一,即是否為boolean, byte, char, short, int, long, float, 和double 等原始類型
Field getField(String name)
獲取指定名稱的域?qū)ο?/p>
AccessibleObject.setAccessible(fields, true)
當(dāng)訪問 Field匙姜、Method 和 Constructor 的時候Java會執(zhí)行訪問檢查畅厢,如果訪問者沒有權(quán)限將拋出SecurityException,譬如訪問者是無法訪問private修飾的域的氮昧。通過設(shè)置 setAccessible(true) 可以取消Java的執(zhí)行訪問檢查框杜,這樣訪問者就獲得了指定 Field、Method 或 Constructor 訪問權(quán)限
Class<?> Field.getType()
返回一個Class 對象袖肥,它標(biāo)識了此 Field 對象所表示字段的聲明類型
Object Field.get(Object obj)
獲取obj對象上當(dāng)前域?qū)ο蟊硎镜膶傩缘膶嶋H值咪辱,獲取到的是一個Object對象,實際使用中還需要轉(zhuǎn)換成實際的類型椎组,或者可以通過 getByte()油狂、getChar、getInt() 等直接獲取具體類型的值
void Field.set(Object obj, Object value)
設(shè)置obj對象上當(dāng)前域表示的屬性的實際值
查看對象數(shù)據(jù)域?qū)嶋H內(nèi)容程序
了解完上述相關(guān)API之后庐杨,我們敲出下面的程序來驗證
public class ObjectAnalyzer {
private ArrayList<Object> visited = new ArrayList<>();
public String toString(Object obj) {
if (obj == null) {
return "null";
}
if (visited.contains(obj)) { // 如果該對象已經(jīng)處理過选调,則不再處理
return "...";
}
visited.add(obj);
Class cl = obj.getClass(); // 獲取Class對象
if (cl == String.class) { // 如果是String類型則直接轉(zhuǎn)為String
return (String) obj;
}
if (cl.isArray()) { // 如果是數(shù)組
String r = cl.getComponentType() + "[]{\n"; // 數(shù)組的元素的類型
for (int i = 0; i < Array.getLength(obj); i++) {
if (i > 0) { // 不是數(shù)組的第一個元素加逗號和換行夹供,顯示更加美觀
r += ",\n";
}
r += "\t";
Object val = Array.get(obj, i);
if (cl.getComponentType().isPrimitive()) { // Class為8種基本類型的時候為 true灵份,直接輸出
r += val;
} else {
r += toString(val); // 不是8中基本類型時,說明是類哮洽,遞歸調(diào)用toString
}
}
return r + "\n}";
}
// 既不是String填渠,也不是數(shù)組時,輸出該對象的類型和屬性值
String r = cl.getName();
do {
r += "[";
Field[] fields = cl.getDeclaredFields(); // 獲取該類自己定義的所有域鸟辅,包括私有的氛什,不包括父類的
AccessibleObject.setAccessible(fields, true); // 訪問私有的屬性,需要打開這個設(shè)置匪凉,否則會報非法訪問異常
for (Field f : fields) {
if (!Modifier.isStatic(f.getModifiers())) { // 通過 Modifier 可獲取該域的修飾符枪眉,這里判斷是否為 static
if (!r.endsWith("[")) {
r += ",";
}
r += f.getName() + "="; // 域名稱
try {
Class t = f.getType(); // 域(屬性)的類型
Object val = f.get(obj); // 獲取obj對象上該域的實際值
if (t.isPrimitive()) { // 如果類型為8種基本類型,則直接輸出
r += val;
} else {
r += toString(val); // 不是8種基本類型再层,遞歸調(diào)用toString
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
r += "]";
cl = cl.getSuperclass(); // 繼續(xù)打印超類的類信息
} while (cl != null);
return r;
}
}
測試驗證結(jié)果
接下來驗證一下獲取數(shù)據(jù)域?qū)嶋H值是否正確贸铜,分別打印數(shù)組堡纬、自定義類的對象的實際值
public class ObjectAnalyzerTest {
public static void main(String[] args) {
int size = 4;
ArrayList<Integer> squares = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
squares.add(i * i);
}
ObjectAnalyzer objectAnalyzer = new ObjectAnalyzer(); // 創(chuàng)建一個上面定義的分析類ObjectAnalyzer的對象
System.out.println(objectAnalyzer.toString(squares)); // 分析ArrayList<Integer>對象的實際值
Employee employee = new Employee("小明", "18", "愛好寫代碼", 1, "Java攻城獅", 100); // 分析自定義類Employee的對象的實際值
System.out.println(objectAnalyzer.toString(employee));
}
}
輸出如下
java.util.ArrayList[elementData=class java.lang.Object[]{
java.lang.Integer[value=0][][],
java.lang.Integer[value=1][][],
java.lang.Integer[value=4][][],
java.lang.Integer[value=9][][]
},size=4][modCount=4][][]
reflect.Employee[empNo=1,position=Java攻城獅,salary=100][name=小明,age=18,hobby=愛好寫代碼][]
其中ArrayList<Integer>
打印了類名和5個元素的類型和值,Employee
打印了類名蒿秦,自己定義的3個基本類型的屬性的實際值烤镐,和父類Person的3個基本類型的屬性的實際值
需要注意的是,position棍鳖,age 是 protected 保護(hù)域炮叶,salary,hobby 是 private 私有域渡处,Java的安全機(jī)制只允許查看任意對象有哪些域镜悉,但是不允許讀取它們的值
程序中是通過 AccessibleObject.setAccessible(fields, true)
將域設(shè)置為了可訪問,取消了Java的執(zhí)行訪問檢查医瘫,因此可以訪問积瞒,如果不加會報異常 IllegalAccessException
小結(jié)一下,我們通過 setAccessible(true) 繞過了Java執(zhí)行訪問檢查登下,因此能夠訪問私有域茫孔,通過 Field.getType() 獲得了屬性的聲明類型,通過了 Field.get(Object obj) 獲得了該域?qū)傩缘膶嶋H值被芳,還有一個沒用上的 Field.set(Object obj, Object value) 設(shè)置域?qū)傩缘膶嶋H值
調(diào)用任意方法
上面我們已經(jīng)獲取了類的構(gòu)造器缰贝,方法,域畔濒,查看和設(shè)置了域的實際值剩晴,那么是不是還可以在調(diào)用對象的方法呢?嘿嘿侵状,又猜對了赞弥,機(jī)智,類的方法信息趣兄,獲取都獲取了绽左,當(dāng)然就要調(diào)用一下,來都來了
上面查看Field的實際值是通過 Field 類的 get() 方法艇潭,與之類似拼窥,Method 調(diào)用方法是通過 Method 類的 invoke 方法
調(diào)用任意方法相關(guān)的API
Method getMethod(String name, Class<?>... parameterTypes)
獲取指定的 Method,參數(shù) name 為要獲取的方法名蹋凝,parameterTypes 為指定方法的參數(shù)的 Class鲁纠,由于可能存在多個同名的重載方法,所以只有提供正確的 parameterTypes 才能準(zhǔn)確的獲取到指定的 Method
Object invoke(Object obj, Object... args)
執(zhí)行方法鳍寂,第一個參數(shù)執(zhí)行該方法的對象改含,如果是static修飾的類方法,則傳null即可迄汛;后面是傳給該方法執(zhí)行的具體的參數(shù)值
調(diào)用任意方法程序
public class MethodTableTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Employee employee = new Employee("小明", "18", "寫代碼", 1, "Java攻城獅", 100000);
Method sayHello = employee.getClass().getMethod("sayHello");
System.out.println(sayHello); // 打印 sayHello 的方法信息
sayHello.invoke(employee); // 讓 employee 執(zhí)行 sayHello 方法
double x = 3.0;
Method square = MethodTableTest.class.getMethod("square", double.class); // 獲取 MethodTableTest 的square方法
double y1 = (double) square.invoke(null, x); // 調(diào)用類方法 square 求平方捍壤,方法參數(shù) x
System.out.printf("square %-10.4f -> %10.4f%n", x, y1);
Method sqrt = Math.class.getMethod("sqrt", double.class); // 獲取 Math 的 sqrt 方法
double y2 = (double) sqrt.invoke(null, x); // 調(diào)用類方法 sqrt 求根刃唤,方法參數(shù) x
System.out.printf("sqrt %-10.4f -> %10.4f%n", x, y2);
}
// static靜態(tài)方法 計算乘方
public static double square(double x) {
return x * x;
}
}
執(zhí)行結(jié)果
public void reflect.Employee.sayHello()
Hello, 我是 小明, 今年 18 歲, 愛好是寫代碼, 我目前的工作是Java攻城獅, 月入100000元
square 3.0000 -> 9.0000
sqrt 3.0000 -> 1.7321
相信大家都看懂啦,通過 getMethod() 獲取指定的 Method白群,再調(diào)用 Method.invoke() 執(zhí)行該方法
反射的優(yōu)缺點
此段引用自 CyC2018/CS-Notes
反射的優(yōu)點:
可擴(kuò)展性 :應(yīng)用程序可以利用全限定名創(chuàng)建可擴(kuò)展對象的實例尚胞,來使用來自外部的用戶自定義類。
類瀏覽器和可視化開發(fā)環(huán)境 :一個類瀏覽器需要可以枚舉類的成員帜慢×眩可視化開發(fā)環(huán)境(如 IDE)可以從利用反射中可用的類型信息中受益,以幫助程序員編寫正確的代碼粱玲。
調(diào)試器和測試工具 : 調(diào)試器需要能夠檢查一個類里的私有成員躬柬。測試工具可以利用反射來自動地調(diào)用類里定義的可被發(fā)現(xiàn)的 API 定義,以確保一組測試中有較高的代碼覆蓋率抽减。
反射的缺點:
盡管反射非常強(qiáng)大允青,但也不能濫用。如果一個功能可以不用反射完成卵沉,那么最好就不用颠锉。在我們使用反射技術(shù)時,下面幾條內(nèi)容應(yīng)該牢記于心史汗。
性能開銷 :反射涉及了動態(tài)類型的解析琼掠,所以 JVM 無法對這些代碼進(jìn)行優(yōu)化。因此停撞,反射操作的效率要比那些非反射操作低得多瓷蛙。我們應(yīng)該避免在經(jīng)常被執(zhí)行的代碼或?qū)π阅芤蠛芨叩某绦蛑惺褂梅瓷洹?/p>
安全限制 :使用反射技術(shù)要求程序必須在一個沒有安全限制的環(huán)境中運(yùn)行。如果一個程序必須在有安全限制的環(huán)境中運(yùn)行戈毒,如 Applet艰猬,那么這就是個問題了。
內(nèi)部暴露 :由于反射允許代碼執(zhí)行一些在正常情況下不被允許的操作(比如訪問私有的屬性和方法)埋市,所以使用反射可能會導(dǎo)致意料之外的副作用冠桃,這可能導(dǎo)致代碼功能失調(diào)并破壞可移植性。反射代碼破壞了抽象性恐疲,因此當(dāng)平臺發(fā)生改變的時候腊满,代碼的行為就有可能也隨著變化。
參考:
《Java核心技術(shù)》卷一
https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Java%20%E5%9F%BA%E7%A1%80.md#%E4%B8%83%E5%8F%8D%E5%B0%84
后記
歡迎評論培己、轉(zhuǎn)發(fā)、分享胚泌,您的支持是我最大的動力
更多內(nèi)容可訪問我的個人博客:http://laijianfeng.org
關(guān)注【小旋鋒】微信公眾號省咨,及時接收博文推送