讀懂框架設(shè)計(jì)的靈魂 — Java 反射機(jī)制
本文轉(zhuǎn)載自微信公眾號(hào)「飛天小牛肉」,作者飛天小牛肉 。轉(zhuǎn)載本文請(qǐng)聯(lián)系飛天小牛肉公眾號(hào)。
Java 反射機(jī)制對(duì)于小白來(lái)說(shuō),真的是一道巨大的坎兒,其他的東西吧,無(wú)非就是內(nèi)容多點(diǎn),多看看多背背就好了,反射真的就是不管看了多少遍不理解就還是不理解,而且學(xué)校里面的各大教材應(yīng)該都沒有反射這個(gè)章節(jié),有也是一帶而過。說(shuō)實(shí)話,在這篇文章之前,我對(duì)反射也并非完全了解,畢竟平常開發(fā)基本用不到,不過,看完這篇文章相信你對(duì)反射就沒啥疑點(diǎn)了。
全文脈絡(luò)思維導(dǎo)圖如下:
1. 拋磚引玉:為什么要使用反射前文我們說(shuō)過,接口的使用提高了代碼的可維護(hù)性和可擴(kuò)展性,并且降低了代碼的耦合度。來(lái)看個(gè)例子:
首先,我們擁有一個(gè)接口 X 及其方法 test,和兩個(gè)對(duì)應(yīng)的實(shí)現(xiàn)類 A、B:
- public class Test {
- interface X {
- public void test();
- }
- class A implements X{
- @Override
- public void test() {
- System.out.println("I am A");
- }
- }
- class B implements X{
- @Override
- public void test() {
- System.out.println("I am B");
- }
- }
通常情況下,我們需要使用哪個(gè)實(shí)現(xiàn)類就直接 new 一個(gè)就好了,看下面這段代碼:
- public class Test {
- ......
- public static void main(String[] args) {
- X a = create1("A");
- a.test();
- X b = create1("B");
- b.test();
- }
- public static X create1(String name){
- if (name.equals("A")) {
- return new A();
- } else if(name.equals("B")){
- return new B();
- }
- return null;
- }
- }
按照上面這種寫法,如果有成百上千個(gè)不同的 X 的實(shí)現(xiàn)類需要?jiǎng)?chuàng)建,那我們豈不是就需要寫上千個(gè) if 語(yǔ)句來(lái)返回不同的 X 對(duì)象?
我們來(lái)看看看反射機(jī)制是如何做的:
- public class Test {
- public static void main(String[] args) {
- X a = create2("A");
- a.test();
- X b = create2("B");
- b.testReflect();
- }
- // 使用反射機(jī)制
- public static X create2(String name){
- Class<?> class = Class.forName(name);
- X x = (X) class.newInstance();
- return x;
- }
- }
向 create2() 方法傳入包名和類名,通過反射機(jī)制動(dòng)態(tài)的加載指定的類,然后再實(shí)例化對(duì)象。
看完上面這個(gè)例子,相信諸位對(duì)反射有了一定的認(rèn)識(shí)。反射擁有以下四大功能:
- 在運(yùn)行時(shí)(動(dòng)態(tài)編譯)獲知任意一個(gè)對(duì)象所屬的類。
- 在運(yùn)行時(shí)構(gòu)造任意一個(gè)類的對(duì)象。
- 在運(yùn)行時(shí)獲知任意一個(gè)類所具有的成員變量和方法。
- 在運(yùn)行時(shí)調(diào)用任意一個(gè)對(duì)象的方法和屬性。
上述這種「動(dòng)態(tài)獲取信息、動(dòng)態(tài)調(diào)用對(duì)象的方法」的功能稱為 Java 語(yǔ)言的反射機(jī)制。
2. 理解 Class 類
要想理解反射,首先要理解 Class 類,因?yàn)?Class 類是反射實(shí)現(xiàn)的基礎(chǔ)。
在程序運(yùn)行期間,JVM 始終為所有的對(duì)象維護(hù)一個(gè)被稱為「運(yùn)行時(shí)的類型標(biāo)識(shí)」,這個(gè)信息跟蹤著每個(gè)對(duì)象所屬的類的完整結(jié)構(gòu)信息,包括包名、類名、實(shí)現(xiàn)的接口、擁有的方法和字段等??梢酝ㄟ^專門的 Java 類訪問這些信息,這個(gè)類就是 Class類。我們可以把 Class 類理解為「類的類型」,一個(gè) Class 對(duì)象,稱為類的類型對(duì)象,「一個(gè) Class 對(duì)象對(duì)應(yīng)一個(gè)加載到 JVM 中的一個(gè) .class 文件」。
在通常情況下,一定是先有類再有對(duì)象。以下面這段代碼為例,類的正常加載過程是這樣的:
- import java.util.Date; // 先有類
- public class Test {
- public static void main(String[] args) {
- Date date = new Date(); // 后有對(duì)象
- System.out.println(date);
- }
- }
首先 JVM 會(huì)將你的代碼編譯成一個(gè) .class 字節(jié)碼文件,然后被類加載器(Class Loader)加載進(jìn) JVM 的內(nèi)存中,「同時(shí)會(huì)創(chuàng)建一個(gè) Date 類的 Class 對(duì)象存到堆中」(注意這個(gè)不是 new 出來(lái)的對(duì)象,而是類的類型對(duì)象)。JVM 在創(chuàng)建 Date對(duì)象前,會(huì)先檢查其類是否加載,尋找類對(duì)應(yīng)的 Class 對(duì)象,若加載好,則為其分配內(nèi)存,然后再進(jìn)行初始化 new Date()。
需要注意的是,「每個(gè)類只有一個(gè) Class 對(duì)象」,也就是說(shuō)如果我們有第二條 new Date() 語(yǔ)句,JVM 不會(huì)再生成一個(gè) Date 的 Class 對(duì)象,因?yàn)橐呀?jīng)存在一個(gè)了。這也使得我們可以利用 == 運(yùn)算符實(shí)現(xiàn)兩個(gè)類對(duì)象比較的操作:
- System.out.println(date.getClass() == Date.getClass()); // true
OK,那么在加載完一個(gè)類后,堆內(nèi)存的方法區(qū)就產(chǎn)生了一個(gè) Class 對(duì)象,這個(gè)對(duì)象就包含了完整的類的結(jié)構(gòu)信息,「我們可以通過這個(gè) Class 對(duì)象看到類的結(jié)構(gòu)」,就好比一面鏡子。所以我們形象的稱之為:反射。
說(shuō)的再詳細(xì)點(diǎn),再解釋一下。上文說(shuō)過,在通常情況下,一定是先有類再有對(duì)象,我們把這個(gè)通常情況稱為 “正”。那么反射中的這個(gè) “反” 我們就可以理解為根據(jù)對(duì)象找到對(duì)象所屬的類(對(duì)象的出處)
- Date date = new Date();
- System.out.println(date.getClass()); // "class java.util.Date"
通過反射,也就是調(diào)用了 getClass() 方法后,我們就獲得了 Date 類對(duì)應(yīng)的 Class 對(duì)象,看到了 Date 類的結(jié)構(gòu),輸出了 Date 對(duì)象所屬的類的完整名稱,即找到了對(duì)象的出處。當(dāng)然,獲取 Class 對(duì)象的方式不止這一種。
3. 獲取 Class 類對(duì)象的四種方式
從 Class 類的源碼可以看出,它的構(gòu)造函數(shù)是私有的,也就是說(shuō)只有 JVM 可以創(chuàng)建 Class 類的對(duì)象,我們不能像普通類一樣直接 new 一個(gè) Class 對(duì)象。
我們只能通過已有的類來(lái)得到一個(gè) Class類對(duì)象,Java 提供了四種方式:
「第一種:知道具體類的情況下可以使用」:
- Class alunbarClass = TargetObject.class;
但是我們一般是不知道具體類的,基本都是通過遍歷包下面的類來(lái)獲取 Class 對(duì)象,通過此方式獲取 Class 對(duì)象不會(huì)進(jìn)行初始化。
「第二種:通過 Class.forName()傳入全類名獲取」:
- Class alunbarClass1 = Class.forName("com.xxx.TargetObject");
這個(gè)方法內(nèi)部實(shí)際調(diào)用的是 forName0:
第 2 個(gè) boolean 參數(shù)表示類是否需要初始化,默認(rèn)是需要初始化。一旦初始化,就會(huì)觸發(fā)目標(biāo)對(duì)象的 static 塊代碼執(zhí)行,static 參數(shù)也會(huì)被再次初始化。
「第三種:通過對(duì)象實(shí)例 instance.getClass() 獲取」:
- Date date = new Date();
- Class alunbarClass2 = date.getClass(); // 獲取該對(duì)象實(shí)例的 Class 類對(duì)象
「第四種:通過類加載器 xxxClassLoader.loadClass() 傳入類路徑獲取」
- class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");
通過類加載器獲取 Class 對(duì)象不會(huì)進(jìn)行初始化,意味著不進(jìn)行包括初始化等一些列步驟,靜態(tài)塊和靜態(tài)對(duì)象不會(huì)得到執(zhí)行。這里可以和 forName 做個(gè)對(duì)比。
4. 通過反射構(gòu)造一個(gè)類的實(shí)例
上面我們介紹了獲取 Class 類對(duì)象的方式,那么成功獲取之后,我們就需要構(gòu)造對(duì)應(yīng)類的實(shí)例。下面介紹三種方法,第一種最為常見,最后一種大家稍作了解即可。
① 使用 Class.newInstance
舉個(gè)例子:
- Date date1 = new Date();
- Class alunbarClass2 = date1.getClass();
- Date date2 = alunbarClass2.newInstance(); // 創(chuàng)建一個(gè)與 alunbarClass2 具有相同類類型的實(shí)例
創(chuàng)建了一個(gè)與 alunbarClass2 具有相同類類型的實(shí)例。
需要注意的是,「newInstance方法調(diào)用默認(rèn)的構(gòu)造函數(shù)(無(wú)參構(gòu)造函數(shù))初始化新創(chuàng)建的對(duì)象。如果這個(gè)類沒有默認(rèn)的構(gòu)造函數(shù), 就會(huì)拋出一個(gè)異?!埂?/p>
② 通過反射先獲取構(gòu)造方法再調(diào)用
由于不是所有的類都有無(wú)參構(gòu)造函數(shù)又或者類構(gòu)造器是 private 的,在這樣的情況下,如果我們還想通過反射來(lái)實(shí)例化對(duì)象,Class.newInstance 是無(wú)法滿足的。
此時(shí),我們可以使用 Constructor 的 newInstance 方法來(lái)實(shí)現(xiàn),先獲取構(gòu)造函數(shù),再執(zhí)行構(gòu)造函數(shù)。
從上面代碼很容易看出,Constructor.newInstance 是可以攜帶參數(shù)的,而 Class.newInstance 是無(wú)參的,這也就是為什么它只能調(diào)用無(wú)參構(gòu)造函數(shù)的原因了。
大家不要把這兩個(gè) newInstance 方法弄混了。如果被調(diào)用的類的構(gòu)造函數(shù)為默認(rèn)的構(gòu)造函數(shù),采用Class.newInstance() 是比較好的選擇, 一句代碼就 OK;如果需要調(diào)用類的帶參構(gòu)造函數(shù)、私有構(gòu)造函數(shù)等, 就需要采用 Constractor.newInstance()
Constructor.newInstance 是執(zhí)行構(gòu)造函數(shù)的方法。我們來(lái)看看獲取構(gòu)造函數(shù)可以通過哪些渠道,作用如其名,以下幾個(gè)方法都比較好記也容易理解,返回值都通過 Cnostructor 類型來(lái)接收。
「批量獲取構(gòu)造函數(shù)」:
1)獲取所有"公有的"構(gòu)造方法
- public Constructor[] getConstructors() { }
2)獲取所有的構(gòu)造方法(包括私有、受保護(hù)、默認(rèn)、公有)
- public Constructor[] getDeclaredConstructors() { }
「單個(gè)獲取構(gòu)造函數(shù)」:
1)獲取一個(gè)指定參數(shù)類型的"公有的"構(gòu)造方法
- public Constructor getConstructor(Class... parameterTypes) { }
2)獲取一個(gè)指定參數(shù)類型的"構(gòu)造方法",可以是私有的,或受保護(hù)、默認(rèn)、公有
- public Constructor getDeclaredConstructor(Class... parameterTypes) { }
舉個(gè)例子:
- package fanshe;
- public class Student {
- //(默認(rèn)的構(gòu)造方法)
- Student(String str){
- System.out.println("(默認(rèn))的構(gòu)造方法 s = " + str);
- }
- // 無(wú)參構(gòu)造方法
- public Student(){
- System.out.println("調(diào)用了公有、無(wú)參構(gòu)造方法執(zhí)行了。。。");
- }
- // 有一個(gè)參數(shù)的構(gòu)造方法
- public Student(char name){
- System.out.println("姓名:" + name);
- }
- // 有多個(gè)參數(shù)的構(gòu)造方法
- public Student(String name ,int age){
- System.out.println("姓名:"+name+"年齡:"+ age);//這的執(zhí)行效率有問題,以后解決。
- }
- // 受保護(hù)的構(gòu)造方法
- protected Student(boolean n){
- System.out.println("受保護(hù)的構(gòu)造方法 n = " + n);
- }
- // 私有構(gòu)造方法
- private Student(int age){
- System.out.println("私有的構(gòu)造方法年齡:"+ age);
- }
- }
- ----------------------------------
- public class Constructors {
- public static void main(String[] args) throws Exception {
- // 加載Class對(duì)象
- Class clazz = Class.forName("fanshe.Student");
- // 獲取所有公有構(gòu)造方法
- Constructor[] conArray = clazz.getConstructors();
- for(Constructor c : conArray){
- System.out.println(c);
- }
- // 獲取所有的構(gòu)造方法(包括:私有、受保護(hù)、默認(rèn)、公有)
- conArray = clazz.getDeclaredConstructors();
- for(Constructor c : conArray){
- System.out.println(c);
- }
- // 獲取公有、無(wú)參的構(gòu)造方法
- // 因?yàn)槭菬o(wú)參的構(gòu)造方法所以類型是一個(gè)null,不寫也可以:這里需要的是一個(gè)參數(shù)的類型,切記是類型
- // 返回的是描述這個(gè)無(wú)參構(gòu)造函數(shù)的類對(duì)象。
- Constructor con = clazz.getConstructor(null);
- Object obj = con.newInstance(); // 調(diào)用構(gòu)造方法
- // 獲取私有構(gòu)造方法
- con = clazz.getDeclaredConstructor(int.class);
- System.out.println(con);
- con.setAccessible(true); // 為了調(diào)用 private 方法/域 我們需要取消安全檢查
- obj = con.newInstance(12); // 調(diào)用構(gòu)造方法
- }
- }
③ 使用開源庫(kù) Objenesis
Objenesis 是一個(gè)開源庫(kù),和上述第二種方法一樣,可以調(diào)用任意的構(gòu)造函數(shù),不過封裝的比較簡(jiǎn)潔:
- public class Test {
- // 不存在無(wú)參構(gòu)造函數(shù)
- private int i;
- public Test(int i){
- this.i = i;
- }
- public void show(){
- System.out.println("test..." + i);
- }
- }
- ------------------------
- public static void main(String[] args) {
- Objenesis objenesis = new ObjenesisStd(true);
- Test test = objenesis.newInstance(Test.class);
- test.show();
- }
使用非常簡(jiǎn)單,Objenesis 由子類 ObjenesisObjenesisStd實(shí)現(xiàn)。詳細(xì)源碼此處就不深究了,了解即可。
5. 通過反射獲取成員變量并使用
和獲取構(gòu)造函數(shù)差不多,獲取成員變量也分批量獲取和單個(gè)獲取。返回值通過Field 類型來(lái)接收。
「批量獲取」:
1)獲取所有公有的字段
- public Field[] getFields() { }
2)獲取所有的字段(包括私有、受保護(hù)、默認(rèn)的)
- public Field[] getDeclaredFields() { }
「單個(gè)獲取」:
1)獲取一個(gè)指定名稱的公有的字段
- public Field getField(String name) { }
2)獲取一個(gè)指定名稱的字段,可以是私有、受保護(hù)、默認(rèn)的
- public Field getDeclaredField(String name) { }
獲取到成員變量之后,如何修改它們的值呢?
set 方法包含兩個(gè)參數(shù):
- obj:哪個(gè)對(duì)象要修改這個(gè)成員變量
- value:要修改成哪個(gè)值
舉個(gè)例子:
- package fanshe.field;
- public class Student {
- public Student(){
- }
- public String name;
- protected int age;
- char sex;
- private String phoneNum;
- @Override
- public String toString() {
- return "Student [name=" + name + ", age=" + age + ", sex=" + sex
- + ", phoneNum=" + phoneNum + "]";
- }
- }
- ----------------------------------
- public class Fields {
- public static void main(String[] args) throws Exception {
- // 獲取 Class 對(duì)象
- Class stuClass = Class.forName("fanshe.field.Student");
- // 獲取公有的無(wú)參構(gòu)造函數(shù)
- Constructor con = stuClass.getConstructor();
- // 獲取私有構(gòu)造方法
- con = clazz.getDeclaredConstructor(int.class);
- System.out.println(con);
- con.setAccessible(true); // 為了調(diào)用 private 方法/域 我們需要取消安全檢查
- obj = con.newInstance(12); // 調(diào)用構(gòu)造方法
- // 獲取所有公有的字段
- Field[] fieldArray = stuClass.getFields();
- for(Field f : fieldArray){
- System.out.println(f);
- }
- // 獲取所有的字段 (包括私有、受保護(hù)、默認(rèn)的)
- fieldArray = stuClass.getDeclaredFields();
- for(Field f : fieldArray){
- System.out.println(f);
- }
- // 獲取指定名稱的公有字段
- Field f = stuClass.getField("name");
- Object obj = con.newInstance(); // 調(diào)用構(gòu)造函數(shù),創(chuàng)建該類的實(shí)例
- f.set(obj, "劉德華"); // 為 Student 對(duì)象中的 name 屬性賦值
- // 獲取私有字段
- f = stuClass.getDeclaredField("phoneNum");
- f.setAccessible(true); // 暴力反射,解除私有限定
- f.set(obj, "18888889999"); // 為 Student 對(duì)象中的 phoneNum 屬性賦值
- }
- }
6. 通過反射獲取成員方法并調(diào)用
同樣的,獲取成員方法也分批量獲取和單個(gè)獲取。返回值通過 Method 類型來(lái)接收。
「批量獲取」:
1)獲取所有"公有方法"(包含父類的方法,當(dāng)然也包含 Object 類)
- public Method[] getMethods() { }
2)獲取所有的成員方法,包括私有的(不包括繼承的)
- public Method[] getDeclaredMethods() { }
「單個(gè)獲取」:
獲取一個(gè)指定方法名和參數(shù)類型的成員方法:
- public Method getMethod(String name, Class<?>... parameterTypes)
獲取到方法之后該怎么調(diào)用它們呢?
invoke 方法中包含兩個(gè)參數(shù):
- obj:哪個(gè)對(duì)象要來(lái)調(diào)用這個(gè)方法
- args:調(diào)用方法時(shí)所傳遞的實(shí)參
舉個(gè)例子:
- package fanshe.method;
- public class Student {
- public void show1(String s){
- System.out.println("調(diào)用了:公有的,String參數(shù)的show1(): s = " + s);
- }
- protected void show2(){
- System.out.println("調(diào)用了:受保護(hù)的,無(wú)參的show2()");
- }
- void show3(){
- System.out.println("調(diào)用了:默認(rèn)的,無(wú)參的show3()");
- }
- private String show4(int age){
- System.out.println("調(diào)用了,私有的,并且有返回值的,int參數(shù)的show4(): age = " + age);
- return "abcd";
- }
- }
- -------------------------------------------
- public class MethodClass {
- public static void main(String[] args) throws Exception {
- // 獲取 Class對(duì)象
- Class stuClass = Class.forName("fanshe.method.Student");
- // 獲取公有的無(wú)參構(gòu)造函數(shù)
- Constructor con = stuClass.getConstructor();
- // 獲取所有公有方法
- stuClass.getMethods();
- Method[] methodArray = stuClass.getMethods();
- for(Method m : methodArray){
- System.out.println(m);
- }
- // 獲取所有的方法,包括私有的
- methodArray = stuClass.getDeclaredMethods();
- for(Method m : methodArray){
- System.out.println(m);
- }
- // 獲取公有的show1()方法
- Method m = stuClass.getMethod("show1", String.class);
- System.out.println(m);
- Object obj = con.newInstance(); // 調(diào)用構(gòu)造函數(shù),實(shí)例化一個(gè) Student 對(duì)象
- m.invoke(obj, "小牛肉");
- // 獲取私有的show4()方法
- m = stuClass.getDeclaredMethod("show4", int.class);
- m.setAccessible(true); // 解除私有限定
- Object result = m.invoke(obj, 20);
- System.out.println("返回值:" + result);
- }
- }
7. 反射機(jī)制優(yōu)缺點(diǎn)
「優(yōu)點(diǎn)」:比較靈活,能夠在運(yùn)行時(shí)動(dòng)態(tài)獲取類的實(shí)例。
「缺點(diǎn)」:
1)性能瓶頸:反射相當(dāng)于一系列解釋操作,通知 JVM 要做的事情,性能比直接的 Java 代碼要慢很多。
2)安全問題:反射機(jī)制破壞了封裝性,因?yàn)橥ㄟ^反射可以獲取并調(diào)用類的私有方法和字段。
8. 反射的經(jīng)典應(yīng)用場(chǎng)景
反射在我們實(shí)際編程中其實(shí)并不會(huì)直接大量的使用,但是實(shí)際上有很多設(shè)計(jì)都與反射機(jī)制有關(guān),比如:
- 動(dòng)態(tài)代理機(jī)制
- 使用 JDBC 連接數(shù)據(jù)庫(kù)
- Spring / Hibernate 框架(實(shí)際上是因?yàn)槭褂昧藙?dòng)態(tài)代理,所以才和反射機(jī)制有關(guān))
為什么說(shuō)動(dòng)態(tài)代理使用了反射機(jī)制,下篇文章會(huì)給出詳細(xì)解釋。
JDBC 連接數(shù)據(jù)庫(kù)
在 JDBC 的操作中,如果要想進(jìn)行數(shù)據(jù)庫(kù)的連接,則必須按照以下幾步完成:
- 通過 Class.forName() 加載數(shù)據(jù)庫(kù)的驅(qū)動(dòng)程序 (通過反射加載)
- 通過 DriverManager 類連接數(shù)據(jù)庫(kù),參數(shù)包含數(shù)據(jù)庫(kù)的連接地址、用戶名、密碼
- 通過 Connection 接口接收連接
- 關(guān)閉連接
- public static void main(String[] args) throws Exception {
- Connection con = null; // 數(shù)據(jù)庫(kù)的連接對(duì)象
- // 1. 通過反射加載驅(qū)動(dòng)程序
- Class.forName("com.mysql.jdbc.Driver");
- // 2. 連接數(shù)據(jù)庫(kù)
- con = DriverManager.getConnection(
- "jdbc:mysql://localhost:3306/test","root","root");
- // 3. 關(guān)閉數(shù)據(jù)庫(kù)連接
- con.close();
- }
Spring 框架
反射機(jī)制是 Java 框架設(shè)計(jì)的靈魂,框架的內(nèi)部都已經(jīng)封裝好了,我們自己基本用不著寫。典型的除了Hibernate 之外,還有 Spring 也用到了很多反射機(jī)制,最典型的就是 Spring 通過 xml 配置文件裝載 Bean(創(chuàng)建對(duì)象),也就是 「Spring 的 IoC」,過程如下:
- 加載配置文件,獲取 Spring 容器
- 使用反射機(jī)制,根據(jù)傳入的字符串獲得某個(gè)類的 Class 實(shí)例
- // 獲取 Spring 的 IoC 容器,并根據(jù) id 獲取對(duì)象
- public static void main(String[] args) {
- // 1.使用 ApplicationContext 接口加載配置文件,獲取 spring 容器
- ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
- // 2. 使用反射機(jī)制,根據(jù)這個(gè)字符串獲得某個(gè)類的 Class 實(shí)例
- IAccountService aService = (IAccountService) ac.getBean("accountServiceImpl");
- System.out.println(aService);
- }
另外,「Spring AOP 由于使用了動(dòng)態(tài)代理,所以也使用了反射機(jī)制」,這點(diǎn)我會(huì)在 Spring 的系列文章中詳細(xì)解釋。