@TOC
帶你攻破你很可能存在的Java技術(shù)盲點(diǎn)之動(dòng)態(tài)性技術(shù)原理指南
本系列技術(shù)專題的相關(guān)技術(shù)指南主要有以下三個(gè)方面:
[圖片上傳失敗...(image-2803ea-1687355030565)]
編程語(yǔ)言的類型
學(xué)習(xí)一門新的動(dòng)態(tài)類型語(yǔ)言可能需要花費(fèi)較長(zhǎng)的時(shí)間俱恶,使得已經(jīng)熟悉Java的開發(fā)人員更希望繼續(xù)使用Java來(lái)解決問(wèn)題痢虹。然而善已,Java本身也支持動(dòng)態(tài)性,在一些需要靈活性的場(chǎng)合可以發(fā)揮作用案淋。反射API就是Java中的一個(gè)例子,它能夠在運(yùn)行時(shí)通過(guò)方法名稱查找并調(diào)用方法鞭衩。Java語(yǔ)言也在不斷更新版本充包,提高對(duì)動(dòng)態(tài)性和靈活性的支持。
整體的編程語(yǔ)言分為三大類:靜態(tài)類型語(yǔ)言和動(dòng)態(tài)類型語(yǔ)言山析、半靜態(tài)半動(dòng)態(tài)類型語(yǔ)言堰燎。
[圖片上傳失敗...(image-2210ae-1687355030566)]
靜態(tài)類型語(yǔ)言
Java語(yǔ)言是一種靜態(tài)類型的編程語(yǔ)言,即要在編譯時(shí)進(jìn)行類型檢查笋轨。在Java中秆剪,每個(gè)變量的類型需要在聲明時(shí)顯式指定;所有變量爵政、方法的參數(shù)和返回值的類型必須在程序運(yùn)行之前就已經(jīng)確定仅讽。這種靜態(tài)類型特性使得編譯器能夠在編譯時(shí)進(jìn)行大量的類型檢查,從而發(fā)現(xiàn)代碼中明顯的類型錯(cuò)誤钾挟。然而洁灵,這也意味著代碼中包含了大量不必要的類型聲明,使代碼顯得過(guò)于冗長(zhǎng)且不夠靈活掺出。相對(duì)應(yīng)的徽千,動(dòng)態(tài)類型語(yǔ)言(如JavaScript和Ruby等)的類型檢查則是在運(yùn)行時(shí)進(jìn)行的。在這類語(yǔ)言中蛛砰,源代碼中的變量類型可以在運(yùn)行時(shí)動(dòng)態(tài)確定罐栈。
動(dòng)態(tài)類型語(yǔ)言
相比于靜態(tài)類型語(yǔ)言,動(dòng)態(tài)類型語(yǔ)言(如JavaScript和Ruby等)的類型檢查是在運(yùn)行時(shí)進(jìn)行的泥畅。在這類語(yǔ)言中,源代碼中不需要顯式地聲明類型,因此位仁,使用動(dòng)態(tài)類型語(yǔ)言編寫的代碼更加簡(jiǎn)潔柑贞。近年來(lái),動(dòng)態(tài)類型語(yǔ)言的流行也反映了語(yǔ)言中動(dòng)態(tài)性的重要性聂抢。適當(dāng)?shù)膭?dòng)態(tài)性對(duì)于提高開發(fā)效率非常有幫助钧嘶,因?yàn)樗梢詼p少開發(fā)人員需要編寫的代碼量。
技術(shù)核心方向
雖然Java是一種靜態(tài)類型語(yǔ)言琳疏,但是它也提供了使代碼更具靈活性的動(dòng)態(tài)性特性有决。這些特性包括腳本語(yǔ)言支持API、反射API空盼、動(dòng)態(tài)代理和JSR292中引入的動(dòng)態(tài)語(yǔ)言支持书幕。開發(fā)人員可以選擇不同的方式來(lái)提高代碼的靈活性。例如揽趾,可以使用腳本語(yǔ)言支持API將腳本語(yǔ)言集成到Java程序中台汇,使用反射API在運(yùn)行時(shí)動(dòng)態(tài)調(diào)用方法,使用動(dòng)態(tài)代理攔截接口方法調(diào)用篱瞎,或使用JSR292中的方法句柄來(lái)實(shí)現(xiàn)更多的功能苟呐。方法句柄支持多種變換操作,并能滿足不同場(chǎng)合的需求俐筋。[圖片上傳失敗...(image-cb7441-1687355030566)]
反射API
反射API是Java語(yǔ)言提供的動(dòng)態(tài)性支持牵素,它允許程序在運(yùn)行時(shí)獲取Java類的內(nèi)部結(jié)構(gòu),如構(gòu)造方法澄者、域和方法等两波,并與它們進(jìn)行交互。反射API也能實(shí)現(xiàn)許多動(dòng)態(tài)語(yǔ)言常用的實(shí)用功能闷哆。按照面向?qū)ο蟮乃悸费埽瑧?yīng)該通過(guò)方法來(lái)改變對(duì)象的狀態(tài),而不是直接修改屬性的值抱怔。Java類中的屬性設(shè)置和獲取方法名通常遵循JavaBeans規(guī)范劣坊,以setXxx和getXxx命名。因此屈留,可以編寫一個(gè)工具類局冰,用于設(shè)置和獲取任何符合JavaBeans規(guī)范的對(duì)象的屬性。
可以使用Java的反射API實(shí)現(xiàn)與JavaScript語(yǔ)言的實(shí)現(xiàn)類似的功能灌危,代碼量上并不太有差別康二。實(shí)現(xiàn)思路是先從對(duì)象的類中查找方法,再調(diào)用該方法并傳入?yún)?shù)勇蝙。這個(gè)靜態(tài)方法可以被作為一個(gè)實(shí)用工具方法在程序中使用沫勿。
public class ReflectSetter
public static void invokeSetter(Object obj,String field,Object value) throws NoSuchMethodException,InvocationTargetException,IllegalAccessException{
String methodName "set"+field.substring(0,1).toUppercase() + field.substring(1);
class<?>clazz obj.getclass();
Method method clazz.getMethod (methodName,value.getclass ())
method.invoke (obj,value);
}
}
從上述示例可以看出,反射API可以實(shí)現(xiàn)Java語(yǔ)言的靈活使用。實(shí)際上产雹,反射API定義了提供者和使用者之間的松散契約诫惭,這種契約可以在方法調(diào)用時(shí)只需要建立在名稱和參數(shù)類型上,而不需要在代碼中首先聲明變量蔓挖。這種方式提供了更大的靈活性和動(dòng)態(tài)性夕土,但也需要開發(fā)者自己保證調(diào)用的合法性。如果方法調(diào)用不合法瘟判,相關(guān)的異常會(huì)在運(yùn)行時(shí)拋出怨绣。
反射案例介紹
反射API常用于方法名或?qū)傩悦凑仗囟ㄒ?guī)則變化的情況:
- 在Servlet中,利用反射API可以遍歷HTTP請(qǐng)求中的所有參數(shù)拷获,然后用invokeSetter方法填充領(lǐng)域?qū)ο蟮膶傩灾怠?/li>
- 在數(shù)據(jù)庫(kù)操作中篮撑,也通過(guò)反射API實(shí)現(xiàn)從查詢結(jié)果集中創(chuàng)建并填充領(lǐng)域?qū)ο蟮膱?chǎng)景。這些對(duì)應(yīng)關(guān)系都可以通過(guò)反射API來(lái)建立刀诬。
反射功能操作
反射API雖然能為Java程序帶來(lái)靈活性咽扇,但其實(shí)現(xiàn)機(jī)制也會(huì)帶來(lái)性能代價(jià)。通過(guò)反射調(diào)用方法一般比直接在源代碼中編寫的方式慢一到兩個(gè)數(shù)量級(jí)陕壹。雖然隨著Java虛擬機(jī)的改進(jìn)质欲,反射API的性能得到了提升,但在一些對(duì)性能要求高的應(yīng)用中糠馆,需要慎用反射API嘶伟。
[圖片上傳失敗...(image-559c7b-1687355030566)]
獲取構(gòu)造器
可以通過(guò)反射API獲取Java類中的構(gòu)造方法,從而在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建Java對(duì)象又碌。具體步驟如下:
獲取Class類的對(duì)象九昧,可以使用Class.forName方法或者類的.class屬性。
通過(guò)Class類的getConstructors方法獲取所有的公開構(gòu)造方法的列表毕匀,或者使用getConstructor方法根據(jù)參數(shù)類型獲取公開的構(gòu)造方法铸鹰。如果需要獲取類中真正聲明的構(gòu)造方法,可以使用getDeclaredConstructors和getDeclaredConstructor方法皂岔。
得到表示構(gòu)造方法的java.lang.reflect.Constructor對(duì)象之后蹋笼,可以通過(guò)其getName方法獲取構(gòu)造方法的名稱,getParameterTypes方法獲取構(gòu)造方法的參數(shù)類型躁垛,getModifiers方法獲取構(gòu)造方法的修飾符等信息剖毯。
最后,可以使用newInstance方法創(chuàng)建出新的對(duì)象教馆,該方法接受一個(gè)可變參數(shù)列表逊谋,用于傳遞構(gòu)造方法的參數(shù)值。如果構(gòu)造方法沒有參數(shù)土铺,則可以直接調(diào)用newInstance方法胶滋。
需要注意的是板鬓,使用反射API創(chuàng)建對(duì)象的效率較低,應(yīng)該盡量避免在性能要求較高的場(chǎng)景中使用镀钓。
一般的構(gòu)造方法的獲取和使用并沒有什么特殊之處穗熬,需要特別說(shuō)明的是對(duì)參數(shù)長(zhǎng)度可變的構(gòu)造方法和嵌套類(nested class)的構(gòu)造方法的使用镀迂。
長(zhǎng)度可變的參數(shù) - 構(gòu)造方法
如果一個(gè)構(gòu)造方法聲明了長(zhǎng)度可變的參數(shù)丁溅,需要使用對(duì)應(yīng)的數(shù)組類型的 Class 對(duì)象來(lái)獲取該構(gòu)造方法,因?yàn)殚L(zhǎng)度可變的參數(shù)實(shí)際上是通過(guò)數(shù)組來(lái)實(shí)現(xiàn)的探遵。
使用反射 API 獲取參數(shù)長(zhǎng)度可變的構(gòu)造方法
例如窟赏,如果一個(gè)類 VarargsConstructor 的構(gòu)造方法包含 String 類型的可變長(zhǎng)度參數(shù),調(diào)用getDeclaredConstructor 方法時(shí)需要使用 String[].class箱季,否則會(huì)找不到該構(gòu)造方法涯穷。在調(diào)用newInstance 方法時(shí),需要將作為實(shí)際參數(shù)的字符串?dāng)?shù)組先轉(zhuǎn)換為 Object 類型藏雏,以避免方法調(diào)用時(shí)的歧義拷况,這樣編譯器就知道將該字符串?dāng)?shù)組作為一個(gè)可變長(zhǎng)度的參數(shù)來(lái)傳遞。
public class VarargsConstructor {
public VarargsConstructor(String... names) {}
}
public void useVarargsConstructor() throws Exception {
Constructor<VarargsConstructor> constructor = VarargsConstructor.class.
getDeclaredConstructor(String[].class);
constructor.newInstance((Object) new String[]{"A", "B", "C"});
}
獲取嵌套類的構(gòu)造方法時(shí)掘殴,需要區(qū)分靜態(tài)和非靜態(tài)兩種情況赚瘦。
[圖片上傳失敗...(image-e47b7c-1687355030566)]靜態(tài)嵌套類,可以按照一般的方式來(lái)使用奏寨。
非靜態(tài)嵌套類起意,其特殊之處在于它的對(duì)象實(shí)例中都有一個(gè)隱含的對(duì)象引用,指向包含它的外部類對(duì)象病瞳。這個(gè)隱含的對(duì)象引用的存在揽咕,使得非靜態(tài)嵌套類中的代碼可以直接引用外部類中包含的私有域和方法。因此套菜,在獲取非靜態(tài)嵌套類的構(gòu)造方法時(shí)亲善,類型參數(shù)列表的第一個(gè)值必須是外部類的 Class 對(duì)象。
例如逗柴,對(duì)于非靜態(tài)嵌套類 NestedClass蛹头,獲取其構(gòu)造方法時(shí)需要傳入外部類的 Class 對(duì)象作為第一個(gè)參數(shù),以便在創(chuàng)建新對(duì)象時(shí)傳遞外部對(duì)象的引用嚎于。
static class StaticNestedClass {
public StaticNestedClass(String name) {}
}
class NestedClass {
public NestedClass(int count) {}
}
public void useNestedClassConstructor() throws Exception {
Constructor< StaticNestedClass> sncc = StaticNestedClass.class. getDeclaredConstructor(String.class);
sncc.newInstance("Alex");
Constructor<NestedClass> ncc = NestedClass.class.getDeclaredConstructor(ConstructorUsage.class, int.class);
NestedClass ic = ncc.newInstance(this, 3);
}
獲取Field域
通過(guò)反射 API掘而,可以獲取類中的域(field),包括公開的靜態(tài)域和對(duì)象中的實(shí)例域于购。獲取表示域的 java.lang.reflect.Field 類的對(duì)象之后袍睡,就可以獲取和設(shè)置域的值。與獲取構(gòu)造方法的方法類似肋僧,Class 類中也有 4 個(gè)方法用來(lái)獲取域斑胜,分別是 getFields控淡、getField、getDeclaredFields 和 getDeclaredField止潘。
[圖片上傳失敗...(image-e34148-1687355030566)]
- getFields 方法返回公開的靜態(tài)域和對(duì)象中的實(shí)例域掺炭;
- getField 方法返回指定名稱的公開的靜態(tài)域或?qū)ο笾械膶?shí)例域;
- getDeclaredFields 方法返回類中所有的域凭戴,包括私有的靜態(tài)域和對(duì)象中的實(shí)例域涧狮;
- getDeclaredField 方法返回指定名稱的域,包括私有的靜態(tài)域和對(duì)象中的實(shí)例域么夫。
使用反射 API 獲取和使用靜態(tài)域和實(shí)例域
獲取和使用靜態(tài)域和實(shí)例域的示例者冤,兩者的區(qū)別在于使用靜態(tài)域時(shí)不需要提供具體的對(duì)象實(shí)例,使用 null 即可档痪。
Field 類中除了操作 Object 的 get 和 set 方法之外涉枫,還有操作基本類型的對(duì)應(yīng)方法,包括 getBoolean / setBoolean腐螟、getByte / setByte愿汰、getChar / setChar、getDouble / setDouble乐纸、getFloat / setFloat衬廷、getInt / setInt 和 getLong / setLong 等
public void useField() throws Exception {
Field fieldCount = FieldContainer.class.getDeclaredField("count");
fieldCount.set(null, 3);
Field fieldName = FieldContainer.class.getDeclaredField("name");
FieldContainer fieldContainer = new FieldContainer();
fieldName.set(fieldContainer, "Bob");
}
總的來(lái)說(shuō),獲取和設(shè)置類中的公開域比較簡(jiǎn)單锯仪,但是無(wú)法通過(guò)反射 API 獲取或操作私有域泵督。
獲取Method方法
最常使用反射 API 的場(chǎng)景是獲取對(duì)象中的方法,并在運(yùn)行時(shí)調(diào)用該方法庶喜。Class 類中有 4 個(gè)方法用來(lái)獲取方法小腊,分別是 getMethods、getMethod久窟、getDeclaredMethods 和 getDeclaredMethod秩冈。這些方法的作用類似于獲取構(gòu)造方法和域的對(duì)應(yīng)方法。通過(guò)獲取表示方法的 java.lang.reflect.Method 類的對(duì)象斥扛,可以查詢?cè)摲椒ǖ脑敿?xì)信息入问,例如方法的參數(shù)和返回值的類型等。使用 invoke 方法可以傳入實(shí)際參數(shù)并調(diào)用該方法稀颁。
獲取和調(diào)用對(duì)象中的公開和私有方法的示例
public void useMethod() throws Exception {
MethodContainer mc = new MethodContainer();
Method publicMethod = MethodContainer.class.getDeclaredMethod("publicMethod");
publicMethod.invoke(mc);
Method privateMethod = MethodContainer.class.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(mc);
}
需要注意的是芬失,在調(diào)用私有方法之前,需要先調(diào)用 Method 類的setAccessible方法來(lái)設(shè)置可以訪問(wèn)的權(quán)限匾灶。與構(gòu)造方法和域不同的是棱烂,通過(guò)反射 API 可以獲取到類中的私有方法。
操作數(shù)組
利用反射API對(duì)數(shù)組進(jìn)行操作的方式有所不同于一般的Java對(duì)象阶女。需要使用java.lang.reflect.Array這個(gè)實(shí)用工具類來(lái)實(shí)現(xiàn)颊糜。該類提供了創(chuàng)建數(shù)組和操作數(shù)組元素的方法哩治。newInstance方法用來(lái)創(chuàng)建新的數(shù)組。第一個(gè)參數(shù)是數(shù)組中元素的類型衬鱼,后面的參數(shù)是數(shù)組的維度信息业筏。
String[] names = ( Array.newInstance(int.class, 3, 3, 3);
double[][][] arrays= (double[][][]) Array.newInstance(double[][].class, 2, 2);
使用反射 API 操作數(shù)組
例如,可以使用下面的示例代碼創(chuàng)建一個(gè)長(zhǎng)度為10的一維String數(shù)組和一個(gè)3x3x3的三維數(shù)組:
public void useArray() {
String[] names = (String[]) Array.newInstance(String.class, 10);
names[0] = "Hello";
Array.set(names, 1, "World");
String str = (String) Array.get(names, 0);
int[][][] matrix1 = (int[][][]) Array.newInstance(int.class, 3, 3, 3);
matrix1[0][0][0] = 1;
int[][][] matrix2 = (int[][][]) Array.newInstance(int[].class, 3, 4);
matrix2[0][0] = new int[10];
matrix2[0][1] = new int[3];
matrix2[0][0][1] = 1;
}
需要注意的是鸟赫,盡管在創(chuàng)建時(shí)只聲明了兩個(gè)維度蒜胖,但是matrix2實(shí)際上也是一個(gè)三維數(shù)組,因?yàn)樗脑仡愋褪莇ouble惯疙。
訪問(wèn)權(quán)限與異常處理
使用反射 API 可以繞過(guò) Java 語(yǔ)言中默認(rèn)的訪問(wèn)控制權(quán)限翠勉,例如訪問(wèn)在另一個(gè)類中聲明的私有方法妖啥。這是通過(guò)調(diào)用繼承自 java.lang.reflect.AccessibleObject 的 setAccessible 方法來(lái)實(shí)現(xiàn)的霉颠。在使用 invoke 方法調(diào)用方法時(shí),如果方法本身拋出異常荆虱,invoke 方法會(huì)拋出 InvocationTargetException 異常來(lái)表示這種情況蒿偎。可以通過(guò) InvocationTargetException 異常的 getCause 方法獲取真正的異常信息來(lái)進(jìn)行調(diào)試怀读。
在 Java 7 中诉位,所有與反射操作相關(guān)的異常類都添加了一個(gè)新的父類 java.lang.ReflectiveOperationException,可以直接捕獲這個(gè)新的異常菜枷。
內(nèi)容總結(jié)
Java反射技術(shù)允許程序在運(yùn)行時(shí)動(dòng)態(tài)地獲取類的信息苍糠、調(diào)用類的方法、訪問(wèn)類的屬性等啤誊,從而提高程序的靈活性和可擴(kuò)展性岳瞭。它可以獲取類的名稱、包名蚊锹、父類瞳筏、接口、構(gòu)造方法牡昆、方法姚炕、屬性等信息,創(chuàng)建對(duì)象丢烘,調(diào)用方法柱宦,訪問(wèn)屬性,實(shí)現(xiàn)動(dòng)態(tài)代理等功能播瞳。Java反射技術(shù)在框架開發(fā)掸刊、ORM框架、動(dòng)態(tài)代理狐史、單元測(cè)試等方面都有著重要的應(yīng)用痒给。但是说墨,由于使用反射技術(shù)需要額外的開銷,因此在性能要求較高的場(chǎng)景下苍柏,應(yīng)該盡量避免使用尼斧。