慶哥聊Java反射機(jī)制

作者 | ithuangqing
來源 | 編碼之外(ID:ithuangqing)
關(guān)于Java反射那些事……
今天咱們一起聊聊Java中的反射,那些你知道的和不知道的……
有人說反射機(jī)制是比較簡單的,你覺得呢?先不說簡單不簡單的,我只告訴你,反射不會,對你后面學(xué)習(xí)框架源碼會有很大影響,但是在以后的工作中可能需要你動手去寫反射的情況也很少,也就是說,如果你說你以后不準(zhǔn)備深入研究一些框架的源碼什么的,那我覺得反射你完全不用學(xué)!
什么是反射
那什么是反射呢?希望你能記住這句話:
Java反射是與Java字節(jié)碼相關(guān)的,也就是javac編譯之后的那個class文件
我們使用反射是可以操作這個class字節(jié)碼文件的,具體的操作就包括基本的讀和寫了,咋一看,不明所以然,覺得有點深奧,說簡單點,就是我們可以通過一定的手段去獲取一個類的Class對象,也就是這個類的字節(jié)碼文件,然后使用Class對象自帶的一些API去執(zhí)行一些操作,比如獲取這個對象的一些方法和屬性。
這里,我覺得首先需要理解兩個概念類和對象不知道你們理解的如何,就是啥是類?啥是對象呢?
類和對象
什么是類?寫一段代碼:
public class Person {
//屬性
/**
* 年齡
*/
int age;
/**
* 姓名
*/
String name;
//方法
/**
* eat
*/
public void eating(String what){
System.out.println(age+"歲的"+name+"正在吃"+what);
}
}
在java中,一個類通常包含基本的屬性和方法,所謂的“類”其實也好理解,我們完全可以將其類比為現(xiàn)實世界中的某一類東西,比如香蕉橘子和蘋果都屬于水果這個大類,我們這里拿人類來舉例子,不管你是小明小紅還是麥瑞克,統(tǒng)稱為人類,那么作為一個人類有一些基本的屬性,包括:
姓名 年齡
然后肯定還包含一些特征行為,比如每個人都會吃東西,所以有個行為就是:
吃
這就是作為一個人最基本的屬性和行為,我們寫成java代碼,上述所示那樣,在java中就是最基本的一個類了,也就是上述代碼就定義了一個人類Person出來了,另外一個簡單的理解就是,你可以把這個類想象成是一個模具,這里的Person就相當(dāng)于一個造人模具,通過它我們可以造出來小明,小紅等等,比如我們造出來一個小明:
//新建一個人類,名字叫做小明
Person p = new Person();
p.name = "小明";
p.age = 18;
p.eating("蘋果");
是不是很簡單,只需要給其賦值上具體的姓名年齡和行為就行了,比如上述代碼,我們通過new Person()的方式就創(chuàng)建出來了一個實打?qū)嵉娜藀,只不過目前這個p還沒有具體的屬性和行為,給他制造一些屬性和行為,比如讓他叫做小明,也就是通過**p.name = “小明”**的形式,就這樣,p就成了小明,小明就是p這個Person類產(chǎn)生的實際對象了,一個確切的名叫小明,年齡是18,擁有吃這個行為的對象了。
想象下,模具,模板,生產(chǎn)……
希望以上講述可以讓你清楚的理解什么是類,什么是對象。
Class對象
那我們在看Class對象,我們之前說了,反射就是拿到一個類的字節(jié)碼,然后通過Class自帶的一些API去操作字節(jié)碼從而去做一些事情,那這個Class對象是啥呢?
我們再來看段代碼:
//新建一個人類,名字叫做小紅
Person p1 = new Person();
p1.name = "小紅";
p1.age = 17;
p1.eating("橘子");
是的,我們又創(chuàng)造了小紅出來,然后我們再回過頭來看看創(chuàng)建小明的代碼:
//新建一個人類,名字叫做小明
Person p = new Person();
p.name = "小明";
p.age = 18;
p.eating("蘋果");
不知道你們發(fā)現(xiàn)什么沒有,你看,無論是小明還是小紅,他們的產(chǎn)生都是通過**new Person()**來的,是不是?
之前也說過了,Person相當(dāng)于一個統(tǒng)一的總的人類,是一個模具,通過它我們可以創(chuàng)作出小明小紅甚至任何人,但是萬變不離其宗都要通過Person這個模具來創(chuàng)造,我們再來看一段代碼:
public static void main(String[] args) {
//新建一個人類,名字叫做小明
Person p = new Person();
p.name = "小明";
p.age = 18;
p.eating("蘋果");
//新建一個人類,名字叫做小紅
Person p1 = new Person();
p1.name = "小紅";
p1.age = 17;
p1.eating("橘子");
System.out.println(p.getClass() ** p1.getClass());
}
你知道 System.out.println(p.getClass() ** p1.getClass()); 的輸出結(jié)果是什么嗎?

怎么樣?和你想的是否一樣呢?那么這個getClass獲取的是什么呢?其實就是一個Class對象,也就是Person這個類的Class對象,至于為什么會是true呢?你看,無論是小紅的這個p1還是小明的這個p是不是都是通過Person這個類創(chuàng)建出來的,而無論是p.getClass()還是p1.getClass()獲取的都是這個對象所屬的類的Class對象,那不都是Person這個類的Class對象嘛!
延伸:Class對象是由Class這個類產(chǎn)生的,就如這里的p和p1都是Person對象,而Person對象是由Person類產(chǎn)生的。
好了,到了這里,我們清楚了類和對象,也大致了解了什么是Class對象了,那么我們再看反射,反射就是通過拿到一個類的Class對象從而去操作一些事情,而我們之前說的字節(jié)碼文件,就是javac編譯生成的class文件其實對應(yīng)的就是一個Class對象。
那么我們怎樣去拿到這個Class對象呢?
獲取Class對象的方式
一般來說,我們可以通過三種方式來獲取Class對象:
Class a = Class.forName("完整類名+包名"); Class b = 實例對象.getClass(); Class c = 任何類型.class;
其中第二種我們上面已經(jīng)展示過了,至于第一種其實大家也是比較熟悉的,我們分別來看下這三種:
Class.forName("完整類名+包名")
直接看代碼:
//第一種:Class a = Class.forName("完整類名+包名");
try {
Class a = Class.forName("com.ithuangqing.javase.reflect.Person");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
使用第一種方式的時候要捕獲一個異常就是有可能這個類不存在,這里直接tyr-catch一下即可。
這里需要強(qiáng)調(diào)一下就是這里forName方法,看得出來它是一個靜態(tài)方法是通過類名直接來調(diào)用的,關(guān)于它有以下需要注意事項:
其是一個靜態(tài)方法 方法參數(shù)是個字符串類型 字符串需要的是一個完整的類名 完整類名必須帶有包名
ok,我們接下來看看第二種方式
實例對象.getClass();
其實這種方式我們之前已經(jīng)見到過了,就是這樣的:
//新建一個人類,名字叫做小明
Person p = new Person();
p.name = "小明";
p.age = 18;
p.eating("蘋果");
Class b = p.getClass();
這種方式是通過Person類的實例對象的一個getClass方法來獲取的。
任何類型.class;
接下來我們來看看最后一種,上代碼:
Class c = Person.class;
怎么樣,是不是覺得代碼很簡潔啊,第三種其實利用了對于任何一個類型來說,它都有一個class屬性,比如我們常見的String類,我們也可以通過這種方式去獲取它的Class對象:
Class s = String.class;
也就是說,在Java中任何一種數(shù)據(jù)類型,當(dāng)然也是包括基本數(shù)據(jù)類型的,它們都有這個.class屬性,通過這個我們就可以獲取其Class對象。
獲取Class對象有什么用
那么到了這里,你可能會有疑問了,我們獲取了類型的Class對象之后有什么用呢?
實例化對象
我們之前如果想創(chuàng)建一個對象是怎么做的?是不是通過new的形式,比如這里:
//新建一個人類,名字叫做小明
Person p = new Person();
p.name = "小明";
p.age = 18;
p.eating("蘋果");
我們通過new的形式創(chuàng)建了一個Person對象,那么現(xiàn)在利用反射技術(shù),也就是我們可以通過獲得的Class對象去實例化一個對象,怎么操作的呢?
首先啊,我們在之前的Person類中添加一個無參構(gòu)造方法:
public Person(){
System.out.println("Person類的無參構(gòu)造方法執(zhí)行了");
}
然后們獲取Person類的Class對象:
//拿到Person類的Class對象
Class a = Class.forName("com.ithuangqing.javase.reflect.Person");
//調(diào)用Class對象的一個newInstance方法
Object o = a.newInstance();
System.out.println(o);
這里多了一步操作,就是調(diào)用了Class對象的一個newInstance方法,有什么用呢?我們看下輸出:

發(fā)現(xiàn)Person類的無參構(gòu)造被執(zhí)行了,說明啥,說明這個方法實例化了一個Person對象啊,另外加入我們把Person這里的無參構(gòu)造給刪除了,這里還能執(zhí)行嗎?
實際情況是還可以的,因為對于一個類來說,如果你沒有寫構(gòu)造函數(shù)的話,那么默認(rèn)都是有一個無參構(gòu)造的,但是如果這樣呢?
public Person(String s){
System.out.println("Person類的有參構(gòu)造方法執(zhí)行了");
}
我們在Person類里面添加了一個有參構(gòu)造方法,但是沒有寫無參構(gòu)造,那么這個時候在執(zhí)行這個newInstance會是個什么情況呢?

看到?jīng)],實例化異常,說沒有找到那個初始化方法,啥意思嘞,就是這個newInstance會調(diào)用類的無參構(gòu)造來完成對象的創(chuàng)建,記住了,必須調(diào)用無參構(gòu)造,否則出錯。
在保證有無參構(gòu)造的前提下,我們通過獲得的Class對象調(diào)用newInstance方法就創(chuàng)建了一個對象。
多此一舉?
到了這里,不知道會不會有人感到疑惑,這不是多此一舉嗎?不信你看:
Person person = new Person();
感覺反射的方式好麻煩??!但是,我告訴你,使用new的形式是沒有使用反射的方式靈活的,不信我給你演示看看。
首先呢,我們先寫個配置文件叫做classinfo.properties:

這里面我們定義一個className,然后我們?nèi)プx取這個文件,代碼如下:
FileReader reader = new FileReader("classinfo.properties");
Properties pro = new Properties();
pro.load(reader);
reader.close();
String className = pro.getProperty("className");
System.out.println(className);
以上代碼屬于IO流方面的知識,不懂的可以針對學(xué)習(xí),如此一來我們就得到了我們寫在配置文件中的className屬性了。
那么你還記得我們獲取Class對象的第一種方式嗎,是不是就是這樣:
Class p3 = Class.forName(className);
看到了嘛?然后我們再調(diào)用newInstance方法就可以創(chuàng)建這個Person對象了:
Class p3 = Class.forName(className);
Object o = p3.newInstance();
System.out.println(o);
輸出看看:

你看,這里是不是創(chuàng)建了一個Person對象,那你可能還有疑問,這怎么就靈活了呢?你別著急,接下來我們?nèi)ジ膭舆@個:

我們把配置文件中的className改成Object類型,然后我們再輸出看看:

可以看到,我們在沒有改動任何代碼知識改動了配置文件的情況下就又生成了一個Object對象,也就是我們只需要改動配置文件來生成我們想要的對象即可。
而你要知道的是,改動代碼的風(fēng)險要比改動配置文件的風(fēng)險大得多,所以這樣的做法不僅靈活而且安全性更高,你要知道new的方式可是直接寫死的。
獲取Filed
除了實例化對象之外,我們還可以通過Class對象來獲取Filed,也就是類中的屬性,首先,我們還是先來看下這個Person類:
/**
* 年齡
*/
public int age;
/**
* 姓名
*/
String name;
在這個Person類中我簡單定義了兩個屬性,那么我們看如下代碼:
Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
Field[] fields = c.getFields();
System.out.println(fields.length);
可以猜測下輸出是什么?

會不會感到疑惑,這里的屬性不是有兩個嘛?這里怎么輸出1呢?我們看下這個輸出的屬性是啥?
Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
//獲取公開的Filed
Field[] fields = c.getFields();
System.out.println(fields.length);
for (Field o : fields){
System.out.println(o.getName());
}

相比你已經(jīng)明白了,就是這里的getFileds獲取的是public修飾的屬性,那有沒有可以獲取全部屬性的呢?但若干有:
//獲取所有的Filed
Field[] declaredFields = c.getDeclaredFields();
System.out.println(declaredFields.length);
for (Field o : declaredFields) {
System.out.println(o.getName());
}
我們輸出來看下:

不知道你發(fā)現(xiàn)沒有,當(dāng)我們獲取到Class對象之后我們就可以通過相應(yīng)的get方法去獲取我們想要的一些東西,比如這里的Filed。
給Filed賦值
我們以上獲取到了Filed,比如這里的name和age:

那么我們可不可以給獲取到的屬性復(fù)制呢?答案當(dāng)然是可以的,首先我們來看,我們可以通過如下的代碼來獲取所有的屬性:
//獲取所有的Filed
Field[] declaredFields = c.getDeclaredFields();
但是你看:

也就是說這個getDeclaredFields還有一個有參方法,顯而易見,是可以獲取單個特定的屬性,那這里的字符串應(yīng)該傳入什么呢?我們看各個屬性之間哪里不同,是不是屬性名稱,比如我們想要獲取name這個屬性,我們就可以這樣操作:
Field name = c.getDeclaredField("name");
我們輸出打印這個name看下:

你看,是不是獲取到了這幾個name屬性,那么接下來我想給這個name進(jìn)行賦值又該怎么操作呢?一般來說,給屬性賦值可能會使用什么setXXX之類的方法,我們看下這個Filed有沒有相應(yīng)的一個方法:

的確有這樣的一個方法,看參數(shù),需要傳入一個對象然后一個值,那么這里的值應(yīng)該就是賦值的具體值了,而這個對象就是你要給哪個對象的屬性賦值,我們現(xiàn)在只知道是name屬性,要給name屬性賦值,但是也得知道這個name屬性隸屬于哪個對象吧。
那么,怎么獲取這個對象呢?
Object o = c.newInstance();
沒有忘記這個newInstance吧,于是賦值就順理成章了。
name.set(o,"張三");
那么賦值有了,如何讀去這個值呢?想一下是不是要讀取這個對象中的name屬性,同樣有個get方法如下:
System.out.println(name.get(o));
當(dāng)然,打印結(jié)果必定是“張三”

重點Method
以上我們說了關(guān)于通過反射操作Filed,接下來將是重點,也就是Method。我們?nèi)绾蝸聿僮鬟@個Method呢?也就是類中的方法,比如我們想要得到類中的這些方法,我們其實可以這樣操作:
Method[] methods = c.getDeclaredMethods();
以上代碼可以得到類中的方法集合,那么我們是不是可以遍歷得到每個方法的名字呢?
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
輸出過是:

還記得Person中的這個方法嗎?

如此一來我們就拿到了方法的名稱啊,那么我們是不是還可以拿到方法的返回值類型呢?也就是這里的void?
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
System.out.println(method.getReturnType());
}
輸出結(jié)果是:

如果要獲取方法的修飾符呢?
for (Method method : methods) {
System.out.println(method.getName());
System.out.println(method.getReturnType());
System.out.println(Modifier.toString(method.getModifiers()));
}
輸出結(jié)果是:

接著,如果要繼續(xù)回去方法的參數(shù)呢?可能稍微有點不一樣,你看:
//獲取參數(shù),其實是獲取類型
Class[] parameterTypes = method.getParameterTypes();
for (Class cl : parameterTypes) {
System.out.println(cl.getName());
}
我們獲取方法中的參數(shù),其實最主要的就是知道方法參數(shù)的類型,這里就可以通過Method提供的getParameterTypes來獲取方法的參數(shù),這里得到的是一個數(shù)組,那是因為方法參數(shù)可能有多個,打印輸出:

由此可知其方法參數(shù)為一個String類型的參數(shù)。
接下來就是該思考我們?nèi)绾稳フ{(diào)用這個方法了,想一下我們一般是怎么調(diào)用方法的,是不是這樣?
Person p = new Person();
p.eating("蘋果");
那使用反射的形式該如何調(diào)用呢?接下來是重點哦!
調(diào)用方法
首先我們還是獲取Class對象:
Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
之后我們需要獲取到需要調(diào)用的方法,怎么做呢?在此之前,我們先思考一個問題:
在Java中決定一個方法的是什么?
其實就是方法名稱和形參列表,所以我們要想獲取一個具體的方法需要有方法名稱和形參來決定,因為方法可能會出現(xiàn)重載,于是我們看看如何獲取這個方法:

方法名稱是“eating”,然后還有一個String參數(shù),那么我們就可以通過以下方式去獲取這個方法
Method eating = c.getDeclaredMethod("eating",String.class);
以上我們就獲取到了這個要調(diào)用的方法,接著我們就要去真正調(diào)用,也就是去執(zhí)行這個方法了:

這里有個invoke方法就是調(diào)用的意思,要調(diào)用這個方法的話是不是還需要傳入相關(guān)參數(shù),那么這里invoke的第二個參數(shù)是一個可變長度參數(shù),其實就是對應(yīng)方法的參數(shù)了,因為參數(shù)可能有多個,而第一個Object類型呢?
想一想還缺個啥?是不是缺個對象,要知道我們目前只是通過Class對象來獲取的方法,但是不知道具體是哪個對象的這個方法,所以還需要實例化一個對象出來,然后去執(zhí)行invoke調(diào)用

這里需要重點解決一下這行代碼:
eating.invoke(obj, "橘子");
它的意思就是調(diào)用obj對象的eating方法傳入“橘子”這個參數(shù)。
要好好理解了!
然后我們輸出看下:

當(dāng)然我們也可以使用反射給類中屬性進(jìn)行賦值,具體如下:

以上結(jié)果是不是更熟悉了!
關(guān)于Constructor
接下來我們就要來看看關(guān)于反射中的Constructor了。也就是通過反射機(jī)制去調(diào)用構(gòu)造方法去實例化一個對象。
先看看我們之前是如何通過反射去實例化一個對象的:
Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
Object obj = c.newInstance();
以上這種還接的嗎?它其實是去調(diào)用類的無參構(gòu)造函數(shù)去實例化對象,一旦沒有無參構(gòu)造函數(shù)的話以上的實例化過程就會出錯,比如這個樣子:

我們這里添加了有參構(gòu)造函數(shù),那么在繼續(xù)上述代碼的實例化就會出錯:

就是找不到這個無參構(gòu)造,所以實例化失敗,那么這個時候該如何實例化對象呢?就要用到這個Constructor,具體來看

這個時候我們可以通過調(diào)用第二個方法來獲取構(gòu)造函數(shù),另外,大家還需要知道的就是對于構(gòu)造函數(shù)來說,區(qū)別他們的唯一標(biāo)志就是形參了,所以這里可以直接如下操作:

這里有個知識點,就是getConstructor獲取的是public修飾的方法
所以如果這里是這樣的:

以上就會報錯,所以穩(wěn)妥的做法是如下:

當(dāng)我們得到有參構(gòu)造函數(shù)之后同樣的也是調(diào)用newInstance方法,只不過這里要傳入相應(yīng)的參數(shù)了,執(zhí)行輸出:

如此一來,我們就實現(xiàn)了有參構(gòu)造函數(shù)下的對象實例化!
反射機(jī)制的魅力
通過以上反射的學(xué)習(xí),不知道你發(fā)現(xiàn)沒,反射的確很靈活,靈活的原因是它的很多操作都可以通過配置讀取的方式來實現(xiàn),如此一來,我們就可以通過只修改配置文件的情況下去創(chuàng)建不一樣的對象,從而高效的執(zhí)行一些事情。而這一切真的是只需要修改配置而不需要修改java代碼的!
基于此,我們學(xué)習(xí)了Java中關(guān)于反射的相關(guān)知識點,我相信,今天學(xué)完這些知識以后,過不了多久你就會遺忘,因為你可能將會有很長一段時間不會使用到反射,對于反射,我們主要會在以后深入學(xué)習(xí)框架的時候會用到,平常啥的,用到反射的機(jī)會基本上很少。
但是,反射確實一個不可或缺的重要知識,因為你以后想要拔高自己的技術(shù)能力,那你就得去研究框架,研究底層源碼,如果你對反射不了解的話,你是很難讀懂框架源碼的。
所以,反射地位特殊,需熟練掌握!以備日后不時之需!

往期文章:
JDK 8 Stream 數(shù)據(jù)流效率怎么樣?
歡迎加我微信,一起交流學(xué)習(xí)
如果能給個贊和在看那就更好了,轉(zhuǎn)發(fā)是最大的支持!
