<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          不了解 Java 反射機(jī)制?看這篇就行!

          共 12721字,需瀏覽 26分鐘

           ·

          2020-08-27 15:21





          反射是一個(gè)非常重要的知識點(diǎn),在學(xué)習(xí)Spring 框架時(shí),Bean的初始化用到了反射,在破壞單例模式時(shí)也用到了反射,在獲取標(biāo)注的注解時(shí)也會用到反射······

          當(dāng)然了,反射在日常開發(fā)中,我們沒碰到過多少,至少我沒怎么用過。但面試是造火箭現(xiàn)場,可愛的面試官們又怎會輕易地放過我們呢?反射是開源框架中的一個(gè)重要設(shè)計(jì)理念,在源碼分析中少不了它的身影,所以,今天我會盡量用淺顯易懂的語言,讓你去理解下面這幾點(diǎn):

          (1)反射的思想以及它的作用: 概念篇

          (2)反射的基本使用及應(yīng)用場景: 應(yīng)用篇

          (3)使用反射能給我們編碼時(shí)帶來的優(yōu)勢以及存在的缺陷: 分析篇

          反射的思想及作用

          有反必有正,就像世間的陰和陽,計(jì)算機(jī)的0和1一樣。天道有輪回,蒼天...(凈會在這瞎bibi)

          在學(xué)習(xí)反射之前,先來了解正射是什么。我們平常用的最多的 new 方式實(shí)例化對象的方式就是一種正射的體現(xiàn)。假如我需要實(shí)例化一個(gè)HashMap,代碼就會是這樣子。

          Map?map?=?new?HashMap<>();
          map.put(1,?1);

          某一天發(fā)現(xiàn),該段程序不適合用 HashMap 存儲鍵值對,更傾向于用LinkedHashMap存儲。重新編寫代碼后變成下面這個(gè)樣子。

          Map?map?=?new?LinkedHashMap<>();
          map.put(1,?1);

          假如又有一天,發(fā)現(xiàn)數(shù)據(jù)還是適合用 HashMap來存儲,難道又要重新修改源碼嗎?

          發(fā)現(xiàn)問題了嗎?我們每次改變一種需求,都要去重新修改源碼,然后對代碼進(jìn)行編譯,打包,再到 JVM 上重啟項(xiàng)目。這么些步驟下來,效率非常低。


          對于這種需求頻繁變更但變更不大的場景,頻繁地更改源碼肯定是一種不允許的操作,我們可以使用一個(gè)開關(guān),判斷什么時(shí)候使用哪一種數(shù)據(jù)結(jié)構(gòu)。

          public?Map?getMap(String?param)?{
          ????Map?map?=?null;
          ????if?(param.equals("HashMap"))?{
          ????????map?=?new?HashMap<>();
          ????}?else?if?(param.equals("LinkedHashMap"))?{
          ????????map?=?new?LinkedHashMap<>();
          ????}?else?if?(param.equals("WeakHashMap"))?{
          ????????map?=?new?WeakHashMap<>();
          ????}
          ????return?map;
          }

          通過傳入?yún)?shù)param決定使用哪一種數(shù)據(jù)結(jié)構(gòu),可以在項(xiàng)目運(yùn)行時(shí),通過動態(tài)傳入?yún)?shù)決定使用哪一個(gè)數(shù)據(jù)結(jié)構(gòu)。

          如果某一天還想用TreeMap,還是避免不了修改源碼,重新編譯執(zhí)行的弊端。這個(gè)時(shí)候,反射就派上用場了。

          在代碼運(yùn)行之前,我們不確定將來會使用哪一種數(shù)據(jù)結(jié)構(gòu),只有在程序運(yùn)行時(shí)才決定使用哪一個(gè)數(shù)據(jù)類,而反射可以在程序運(yùn)行過程中動態(tài)獲取類信息調(diào)用類方法。通過反射構(gòu)造類實(shí)例,代碼會演變成下面這樣。

          public?Map?getMap(String?className)?{
          ????Class?clazz?=?Class.forName(className);
          ????Consructor?con?=?clazz.getConstructor();
          ????return?(Map)?con.newInstance();
          }

          無論使用什么 Map,只要實(shí)現(xiàn)了Map接口,就可以使用全類名路徑傳入到方法中,獲得對應(yīng)的 Map 實(shí)例。例如java.util.HashMap / java.util.LinkedHashMap····如果要創(chuàng)建其它類例如WeakHashMap,我也不需要修改上面這段源碼

          我們來回顧一下如何從 new 一個(gè)對象引出使用反射的。

          • 在不使用反射時(shí),構(gòu)造對象使用 new 方式實(shí)現(xiàn),這種方式在編譯期就可以把對象的類型確定下來。
          • 如果需求發(fā)生變更,需要構(gòu)造另一個(gè)對象,則需要修改源碼,非常不優(yōu)雅,所以我們通過使用開關(guān),在程序運(yùn)行時(shí)判斷需要構(gòu)造哪一個(gè)對象,在運(yùn)行時(shí)可以變更開關(guān)來實(shí)例化不同的數(shù)據(jù)結(jié)構(gòu)。
          • 如果還有其它擴(kuò)展的類有可能被使用,就會創(chuàng)建出非常多的分支,且在編碼時(shí)不知道有什么其他的類被使用到,假如日后Map接口下多了一個(gè)集合類是xxxHashMap,還得創(chuàng)建分支,此時(shí)引出了反射:可以在運(yùn)行時(shí)才確定使用哪一個(gè)數(shù)據(jù)類,在切換類時(shí),無需重新修改源碼、編譯程序。

          第一章總結(jié):

          • 反射的思想在程序運(yùn)行過程中確定和解析數(shù)據(jù)類的類型。
          • 反射的作用:對于在編譯期無法確定使用哪個(gè)數(shù)據(jù)類的場景,通過反射可以在程序運(yùn)行時(shí)構(gòu)造出不同的數(shù)據(jù)類實(shí)例

          反射的基本使用

          Java 反射的主要組成部分有4個(gè):

          • Class:任何運(yùn)行在內(nèi)存中的所有類都是該 Class 類的實(shí)例對象,每個(gè) Class 類對象內(nèi)部都包含了本來的所有信息。記著一句話,通過反射干任何事,先找 Class 準(zhǔn)沒錯!
          • Field:描述一個(gè)類的屬性,內(nèi)部包含了該屬性的所有信息,例如數(shù)據(jù)類型,屬性名,訪問修飾符······
          • Constructor:描述一個(gè)類的構(gòu)造方法,內(nèi)部包含了構(gòu)造方法的所有信息,例如參數(shù)類型,參數(shù)名字,訪問修飾符······
          • Method:描述一個(gè)類的所有方法(包括抽象方法),內(nèi)部包含了該方法的所有信息,與Constructor類似,不同之處是 Method 擁有返回值類型信息,因?yàn)闃?gòu)造方法是沒有返回值的。

          我總結(jié)了一張腦圖,放在了下面,如果用到了反射,離不開這核心的4個(gè)類,只有去了解它們內(nèi)部提供了哪些信息,有什么作用,運(yùn)用它們的時(shí)候才能易如反掌


          我們在學(xué)習(xí)反射的基本使用時(shí),我會用一個(gè)SmallPineapple類作為模板進(jìn)行說明,首先我們先來熟悉這個(gè)類的基本組成:屬性,構(gòu)造函數(shù)和方法

          public?class?SmallPineapple?{
          ????public?String?name;
          ????public?int?age;
          ????private?double?weight;?//?體重只有自己知道
          ????
          ????public?SmallPineapple()?{}
          ????
          ????public?SmallPineapple(String?name,?int?age)?{
          ????????this.name?=?name;
          ????????this.age?=?age;
          ????}
          ????public?void?getInfo()?{
          ????????System.out.print("["+?name?+?"?的年齡是:"?+?age?+?"]");
          ????}
          }

          反射中的用法有非常非常多,常見的功能有以下這幾個(gè):

          • 在運(yùn)行時(shí)獲取一個(gè)類的 Class 對象
          • 在運(yùn)行時(shí)構(gòu)造一個(gè)類的實(shí)例化對象
          • 在運(yùn)行時(shí)獲取一個(gè)類的所有信息:變量、方法、構(gòu)造器、注解

          獲取類的 Class 對象

          在 Java 中,每一個(gè)類都會有專屬于自己的 Class 對象,當(dāng)我們編寫完.java文件后,使用javac編譯后,就會產(chǎn)生一個(gè)字節(jié)碼文件.class,在字節(jié)碼文件中包含類的所有信息,如屬性構(gòu)造方法方法······當(dāng)字節(jié)碼文件被裝載進(jìn)虛擬機(jī)執(zhí)行時(shí),會在內(nèi)存中生成 Class 對象,它包含了該類內(nèi)部的所有信息,在程序運(yùn)行時(shí)可以獲取這些信息。

          獲取 Class 對象的方法有3種:

          • 類名.class:這種獲取方式只有在編譯前已經(jīng)聲明了該類的類型才能獲取到 Class 對象
          Class?clazz?=?SmallPineapple.class;
          • 實(shí)例.getClass():通過實(shí)例化對象獲取該實(shí)例的 Class 對象
          SmallPineapple?sp?=?new?SmallPineapple();
          Class?clazz?=?sp.getClass();
          • Class.forName(className):通過類的全限定名獲取該類的 Class 對象
          Class?clazz?=?Class.forName("com.bean.smallpineapple");

          拿到 Class對象就可以對它為所欲為了:剝開它的皮(獲取類信息)、指揮它做事(調(diào)用它的方法),看透它的一切(獲取屬性),總之它就沒有隱私了。

          不過在程序中,每個(gè)類的 Class 對象只有一個(gè),也就是說你只有這一個(gè)奴隸。我們用上面三種方式測試,通過三種方式打印各個(gè) Class 對象都是相同的。

          Class?clazz1?=?Class.forName("com.bean.SmallPineapple");
          Class?clazz2?=?SmallPineapple.class;
          SmallPineapple?instance?=?new?SmallPineapple();
          Class?clazz3?=?instance.getClass();
          System.out.println("Class.forName()?==?SmallPineapple.class:"?+?(clazz1?==?clazz2));
          System.out.println("Class.forName()?==?instance.getClass():"?+?(clazz1?==?clazz3));
          System.out.println("instance.getClass()?==?SmallPineapple.class:"?+?(clazz2?==?clazz3));

          內(nèi)存中只有一個(gè) Class 對象的原因要牽扯到 JVM 類加載機(jī)制雙親委派模型,它保證了程序運(yùn)行時(shí),加載類時(shí)每個(gè)類在內(nèi)存中僅會產(chǎn)生一個(gè)Class對象。在這里我不打算詳細(xì)展開說明,可以簡單地理解為 JVM 幫我們保證了一個(gè)類在內(nèi)存中至多存在一個(gè) Class 對象

          構(gòu)造類的實(shí)例化對象

          通過反射構(gòu)造一個(gè)類的實(shí)例方式有2種:

          • Class 對象調(diào)用newInstance()方法
          Class?clazz?=?Class.forName("com.bean.SmallPineapple");
          SmallPineapple?smallPineapple?=?(SmallPineapple)?clazz.newInstance();
          smallPineapple.getInfo();
          //?[null 的年齡是:0]

          即使 SmallPineapple 已經(jīng)顯式定義了構(gòu)造方法,通過 newInstance() ?創(chuàng)建的實(shí)例中,所有屬性值都是對應(yīng)類型的初始值,因?yàn)?newInstance() 構(gòu)造實(shí)例會調(diào)用默認(rèn)無參構(gòu)造器

          • Constructor 構(gòu)造器調(diào)用newInstance()方法
          Class?clazz?=?Class.forName("com.bean.SmallPineapple");
          Constructor?constructor?=?clazz.getConstructor(String.class,?int.class);
          constructor.setAccessible(true);
          SmallPineapple?smallPineapple2?=?(SmallPineapple)?constructor.newInstance("小菠蘿",?21);
          smallPineapple2.getInfo();
          //?[小菠蘿?的年齡是:21]

          通過 getConstructor(Object... paramTypes) 方法指定獲取指定參數(shù)類型的 Constructor, Constructor 調(diào)用 newInstance(Object... paramValues) 時(shí)傳入構(gòu)造方法參數(shù)的值,同樣可以構(gòu)造一個(gè)實(shí)例,且內(nèi)部屬性已經(jīng)被賦值。

          通過Class對象調(diào)用 newInstance() 會走默認(rèn)無參構(gòu)造方法,如果想通過顯式構(gòu)造方法構(gòu)造實(shí)例,需要提前從Class中調(diào)用getConstructor()方法獲取對應(yīng)的構(gòu)造器,通過構(gòu)造器去實(shí)例化對象。

          這些 API 是在開發(fā)當(dāng)中最常遇到的,當(dāng)然還有非常多重載的方法,本文由于篇幅原因,且如果每個(gè)方法都一一講解,我們也記不住,所以用到的時(shí)候去類里面查找就已經(jīng)足夠了。

          獲取一個(gè)類的所有信息

          Class 對象中包含了該類的所有信息,在編譯期我們能看到的信息就是該類的變量、方法、構(gòu)造器,在運(yùn)行時(shí)最常被獲取的也是這些信息。


          獲取類中的變量(Field)

          • Field[] getFields():獲取類中所有被public修飾的所有變量
          • Field getField(String name):根據(jù)變量名獲取類中的一個(gè)變量,該變量必須被public修飾
          • Field[] getDeclaredFields():獲取類中所有的變量,但無法獲取繼承下來的變量
          • Field getDeclaredField(String name):根據(jù)姓名獲取類中的某個(gè)變量,無法獲取繼承下來的變量

          獲取類中的方法(Method)

          • Method[] getMethods():獲取類中被public修飾的所有方法

          • Method getMethod(String name, Class... paramTypes):根據(jù)名字和參數(shù)類型獲取對應(yīng)方法,該方法必須被public修飾

          • Method[] getDeclaredMethods():獲取所有方法,但無法獲取繼承下來的方法

          • Method getDeclaredMethod(String name, Class... paramTypes):根據(jù)名字和參數(shù)類型獲取對應(yīng)方法,無法獲取繼承下來的方法

          獲取類的構(gòu)造器(Constructor)

          • Constuctor[] getConstructors():獲取類中所有被public修飾的構(gòu)造器
          • Constructor getConstructor(Class... paramTypes):根據(jù)參數(shù)類型獲取類中某個(gè)構(gòu)造器,該構(gòu)造器必須被public修飾
          • Constructor[] getDeclaredConstructors():獲取類中所有構(gòu)造器
          • Constructor getDeclaredConstructor(class... paramTypes):根據(jù)參數(shù)類型獲取對應(yīng)的構(gòu)造器

          每種功能內(nèi)部以 Declared 細(xì)分為2類:

          Declared修飾的方法:可以獲取該類內(nèi)部包含的所有變量、方法和構(gòu)造器,但是無法獲取繼承下來的信息

          Declared修飾的方法:可以獲取該類中public修飾的變量、方法和構(gòu)造器,可獲取繼承下來的信息

          如果想獲取類中**所有的(包括繼承)**變量、方法和構(gòu)造器,則需要同時(shí)調(diào)用getXXXs()getDeclaredXXXs()兩個(gè)方法,用Set集合存儲它們獲得的變量、構(gòu)造器和方法,以防兩個(gè)方法獲取到相同的東西。

          例如:要獲取SmallPineapple獲取類中所有的變量,代碼應(yīng)該是下面這樣寫。

          Class?clazz?=?Class.forName("com.bean.SmallPineapple");
          //?獲取?public?屬性,包括繼承
          Field[]?fields1?=?clazz.getFields();
          //?獲取所有屬性,不包括繼承
          Field[]?fields2?=?clazz.getDeclaredFields();
          //?將所有屬性匯總到?set
          Set?allFields?=?new?HashSet<>();
          allFields.addAll(Arrays.asList(fields1));
          allFields.addAll(Arrays.asList(fields2));

          不知道你有沒有發(fā)現(xiàn)一件有趣的事情,如果父類的屬性用protected修飾,利用反射是無法獲取到的。

          protected 修飾符的作用范圍:只允許同一個(gè)包下或者子類訪問,可以繼承到子類。

          getFields() 只能獲取到本類的public屬性的變量值;

          getDeclaredFields() 只能獲取到本類的所有屬性,不包括繼承的;無論如何都獲取不到父類的 protected 屬性修飾的變量,但是它的的確確存在于子類中。

          獲取注解

          獲取注解單獨(dú)擰了出來,因?yàn)樗⒉皇菍儆?Class 對象的一種信息,每個(gè)變量,方法和構(gòu)造器都可以被注解修飾,所以在反射中,F(xiàn)ield,Constructor 和 Method 類對象都可以調(diào)用下面這些方法獲取標(biāo)注在它們之上的注解。

          • Annotation[] getAnnotations():獲取該對象上的所有注解
          • Annotation getAnnotation(Class annotaionClass):傳入注解類型,獲取該對象上的特定一個(gè)注解
          • Annotation[] getDeclaredAnnotations():獲取該對象上的顯式標(biāo)注的所有注解,無法獲取繼承下來的注解
          • Annotation getDeclaredAnnotation(Class annotationClass):根據(jù)注解類型,獲取該對象上的特定一個(gè)注解,無法獲取繼承下來的注解

          只有注解的@Retension標(biāo)注為RUNTIME時(shí),才能夠通過反射獲取到該注解,@Retension 有3種保存策略:

          • SOURCE:只在**源文件(.java)**中保存,即該注解只會保留在源文件中,編譯時(shí)編譯器會忽略該注解,例如 @Override 注解
          • CLASS:保存在字節(jié)碼文件(.class)中,注解會隨著編譯跟隨字節(jié)碼文件中,但是運(yùn)行時(shí)不會對該注解進(jìn)行解析
          • RUNTIME:一直保存到運(yùn)行時(shí)用得最多的一種保存策略,在運(yùn)行時(shí)可以獲取到該注解的所有信息

          像下面這個(gè)例子,SmallPineapple 類繼承了抽象類PineapplegetInfo()方法上標(biāo)識有 @Override 注解,且在子類中標(biāo)注了@Transient注解,在運(yùn)行時(shí)獲取子類重寫方法上的所有注解,只能獲取到@Transient的信息。

          public?abstract?class?Pineapple?{
          ????public?abstract?void?getInfo();
          }
          public?class?SmallPineapple?extends?Pineapple?{
          ????@Transient
          ????@Override
          ????public?void?getInfo()?{
          ????????System.out.print("小菠蘿的身高和年齡是:"?+?height?+?"cm?;?"?+?age?+?"歲");
          ????}
          }

          啟動類Bootstrap獲取 SmallPineapple 類中的 getInfo() 方法上的注解信息:

          public?class?Bootstrap?{
          ????/**
          ?????*?根據(jù)運(yùn)行時(shí)傳入的全類名路徑判斷具體的類對象
          ?????*?@param?path?類的全類名路徑
          ?????*/

          ????public?static?void?execute(String?path)?throws?Exception?{
          ????????Class?obj?=?Class.forName(path);
          ????????Method?method?=?obj.getMethod("getInfo");
          ????????Annotation[]?annotations?=?method.getAnnotations();
          ????????for?(Annotation?annotation?:?annotations)?{
          ????????????System.out.println(annotation.toString());
          ????????}
          ????}
          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????execute("com.pineapple.SmallPineapple");
          ????}
          }
          //[email protected](value=true)

          通過反射調(diào)用方法

          通過反射獲取到某個(gè) Method 類對象后,可以通過調(diào)用invoke方法執(zhí)行。

          • invoke(Oject obj, Object... args):參數(shù)``1指定調(diào)用該方法的**對象**,參數(shù)2`是方法的參數(shù)列表值。

          如果調(diào)用的方法是靜態(tài)方法,參數(shù)1只需要傳入null,因?yàn)殪o態(tài)方法不與某個(gè)對象有關(guān),只與某個(gè)類有關(guān)。

          可以像下面這種做法,通過反射實(shí)例化一個(gè)對象,然后獲取Method方法對象,調(diào)用invoke()指定SmallPineapplegetInfo()方法。

          Class?clazz?=?Class.forName("com.bean.SmallPineapple");
          Constructor?constructor?=?clazz.getConstructor(String.class,?int.class);
          constructor.setAccessible(true);
          SmallPineapple?sp?=?(SmallPineapple)?constructor.newInstance("小菠蘿",?21);
          Method?method?=?clazz.getMethod("getInfo");
          if?(method?!=?null)?{
          ????method.invoke(sp,?null);
          }
          //?[小菠蘿的年齡是:21]

          反射的應(yīng)用場景

          反射常見的應(yīng)用場景這里介紹3個(gè):

          • Spring 實(shí)例化對象:當(dāng)程序啟動時(shí),Spring 會讀取配置文件applicationContext.xml并解析出里面所有的標(biāo)簽實(shí)例化到IOC容器中。
          • 反射 + 工廠模式:通過反射消除工廠中的多個(gè)分支,如果需要生產(chǎn)新的類,無需關(guān)注工廠類,工廠類可以應(yīng)對各種新增的類,反射可以使得程序更加健壯。
          • JDBC連接數(shù)據(jù)庫:使用JDBC連接數(shù)據(jù)庫時(shí),指定連接數(shù)據(jù)庫的驅(qū)動類時(shí)用到反射加載驅(qū)動類

          Spring 的 IOC 容器

          在 Spring 中,經(jīng)常會編寫一個(gè)上下文配置文件applicationContext.xml,里面就是關(guān)于bean的配置,程序啟動時(shí)會讀取該 xml 文件,解析出所有的 標(biāo)簽,并實(shí)例化對象放入IOC容器中。


          <beans?xmlns="http://www.springframework.org/schema/beans"
          ???????xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          ???????xsi:schemaLocation="http://www.springframework.org/schema/beans?http://www.springframework.org/schema/beans/spring-beans.xsd">

          ????<bean?id="smallpineapple"?class="com.bean.SmallPineapple">
          ????????<constructor-arg?type="java.lang.String"?value="小菠蘿"/>
          ????????<constructor-arg?type="int"?value="21"/>
          ????bean>

          beans>

          在定義好上面的文件后,通過ClassPathXmlApplicationContext加載該配置文件,程序啟動時(shí),Spring 會將該配置文件中的所有bean都實(shí)例化,放入 IOC 容器中,IOC 容器本質(zhì)上就是一個(gè)工廠,通過該工廠傳入 標(biāo)簽的id屬性獲取到對應(yīng)的實(shí)例。

          public?class?Main?{
          ????public?static?void?main(String[]?args)?{
          ????????ApplicationContext?ac?=
          ????????????????new?ClassPathXmlApplicationContext("applicationContext.xml");
          ????????SmallPineapple?smallPineapple?=?(SmallPineapple)?ac.getBean("smallpineapple");
          ????????smallPineapple.getInfo();?//?[小菠蘿的年齡是:21]
          ????}
          }

          Spring 在實(shí)例化對象的過程經(jīng)過簡化之后,可以理解為反射實(shí)例化對象的步驟:

          • 獲取Class對象的構(gòu)造器
          • 通過構(gòu)造器調(diào)用 newInstance() 實(shí)例化對象

          當(dāng)然 Spring 在實(shí)例化對象時(shí),做了非常多額外的操作,才能夠讓現(xiàn)在的開發(fā)足夠的便捷且穩(wěn)定

          在之后的文章中會專門寫一篇文章講解如何利用反射實(shí)現(xiàn)一個(gè)簡易版IOC容器,IOC容器原理很簡單,只要掌握了反射的思想,了解反射的常用 API 就可以實(shí)現(xiàn),我可以提供一個(gè)簡單的思路:利用 HashMap 存儲所有實(shí)例,key 代表 標(biāo)簽的 id,value 存儲對應(yīng)的實(shí)例,這對應(yīng)了 Spring IOC容器管理的對象默認(rèn)是單例的。

          反射 + 抽象工廠模式

          傳統(tǒng)的工廠模式,如果需要生產(chǎn)新的子類,需要修改工廠類,在工廠類中增加新的分支

          public?class?MapFactory?{
          ????public?Map?produceMap(String?name)?{
          ????????if?("HashMap".equals(name))?{
          ????????????return?new?HashMap<>();
          ????????}?else?if?("TreeMap".equals(name))?{
          ????????????return?new?TreeMap<>();
          ????????}?//?···
          ????}
          }

          利用反射和工廠模式相結(jié)合,在產(chǎn)生新的子類時(shí),工廠類不用修改任何東西,可以專注于子類的實(shí)現(xiàn),當(dāng)子類確定下來時(shí),工廠也就可以生產(chǎn)該子類了。

          反射 + 抽象工廠的核心思想是:

          • 在運(yùn)行時(shí)通過參數(shù)傳入不同子類的全限定名獲取到不同的 Class 對象,調(diào)用 newInstance() 方法返回不同的子類。細(xì)心的讀者會發(fā)現(xiàn)提到了子類這個(gè)概念,所以反射 + 抽象工廠模式,一般會用于有繼承或者接口實(shí)現(xiàn)關(guān)系。

          例如,在運(yùn)行時(shí)才確定使用哪一種 Map 結(jié)構(gòu),我們可以利用反射傳入某個(gè)具體 Map 的全限定名,實(shí)例化一個(gè)特定的子類。

          public?class?MapFactory?{
          ????/**
          ?????*?@param?className?類的全限定名
          ?????*/

          ????public?Map?produceMap(String?className)?{
          ????????Class?clazz?=?Class.forName(className);
          ????????Map?map?=?clazz.newInstance();
          ????????return?map;
          ????}
          }

          className 可以指定為 java.util.HashMap,或者 java.util.TreeMap 等等,根據(jù)業(yè)務(wù)場景來定。

          JDBC 加載數(shù)據(jù)庫驅(qū)動類

          在導(dǎo)入第三方庫時(shí),JVM不會主動去加載外部導(dǎo)入的類,而是等到真正使用時(shí),才去加載需要的類,正是如此,我們可以在獲取數(shù)據(jù)庫連接時(shí)傳入驅(qū)動類的全限定名,交給 JVM 加載該類。

          public?class?DBConnectionUtil?{
          ????/**?指定數(shù)據(jù)庫的驅(qū)動類?*/
          ????private?static?final?String?DRIVER_CLASS_NAME?=?"com.mysql.jdbc.Driver";
          ????
          ????public?static?Connection?getConnection()?{
          ????????Connection?conn?=?null;
          ????????//?加載驅(qū)動類
          ????????Class.forName(DRIVER_CLASS_NAME);
          ????????//?獲取數(shù)據(jù)庫連接對象
          ????????conn?=?DriverManager.getConnection("jdbc:mysql://···",?"root",?"root");
          ????????return?conn;
          ????}
          }

          在我們開發(fā) SpringBoot 項(xiàng)目時(shí),會經(jīng)常遇到這個(gè)類,但是可能習(xí)慣成自然了,就沒多大在乎,我在這里給你們看看常見的application.yml中的數(shù)據(jù)庫配置,我想你應(yīng)該會恍然大悟吧。


          這里的 driver-class-name,和我們一開始加載的類是不是覺得很相似,這是因?yàn)?strong style="color: rgb(71, 193, 168);">MySQL版本不同引起的驅(qū)動類不同,這體現(xiàn)使用反射的好處:不需要修改源碼,僅加載配置文件就可以完成驅(qū)動類的替換

          在之后的文章中會專門寫一篇文章詳細(xì)地介紹反射的應(yīng)用場景,實(shí)現(xiàn)簡單的IOC容器以及通過反射實(shí)現(xiàn)工廠模式的好處。

          在這里,你只需要掌握反射的基本用法和它的思想,了解它的主要使用場景。

          反射的優(yōu)勢及缺陷

          反射的優(yōu)點(diǎn)

          • 增加程序的靈活性:面對需求變更時(shí),可以靈活地實(shí)例化不同對象

          但是,有得必有失,一項(xiàng)技術(shù)不可能只有優(yōu)點(diǎn)沒有缺點(diǎn),反射也有兩個(gè)比較隱晦的缺點(diǎn)

          • 破壞類的封裝性:可以強(qiáng)制訪問 private 修飾的信息
          • 性能損耗:反射相比直接實(shí)例化對象、調(diào)用方法、訪問變量,中間需要非常多的檢查步驟和解析步驟,JVM無法對它們優(yōu)化。

          增加程序的靈活性

          這里不再用 SmallPineapple 舉例了,我們來看一個(gè)更加貼近開發(fā)的例子:

          • 利用反射連接數(shù)據(jù)庫,涉及到數(shù)據(jù)庫的數(shù)據(jù)源。在 SpringBoot 中一切約定大于配置,想要定制配置時(shí),使用application.properties配置文件指定數(shù)據(jù)源

          角色1 - Java的設(shè)計(jì)者:我們設(shè)計(jì)好DataSource接口,你們其它數(shù)據(jù)庫廠商想要開發(fā)者用你們的數(shù)據(jù)源監(jiān)控?cái)?shù)據(jù)庫,就得實(shí)現(xiàn)我的這個(gè)接口

          角色2 - 數(shù)據(jù)庫廠商

          • MySQL 數(shù)據(jù)庫廠商:我們提供了 com.mysql.cj.jdbc.MysqlDataSource 數(shù)據(jù)源,開發(fā)者可以使用它連接 MySQL。
          • 阿里巴巴廠商:我們提供了 com.alibaba.druid.pool.DruidDataSource 數(shù)據(jù)源,我這個(gè)數(shù)據(jù)源更牛逼,具有頁面監(jiān)控慢SQL日志記錄等功能,開發(fā)者快來用它監(jiān)控 MySQL吧!
          • SQLServer 廠商:我們提供了 com.microsoft.sqlserver.jdbc.SQLServerDataSource 數(shù)據(jù)源,如果你想實(shí)用SQL Server 作為數(shù)據(jù)庫,那就使用我們的這個(gè)數(shù)據(jù)源連接吧

          角色3 - 開發(fā)者:我們可以用配置文件指定使用DruidDataSource數(shù)據(jù)源

          spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

          需求變更:某一天,老板來跟我們說,Druid 數(shù)據(jù)源不太符合我們現(xiàn)在的項(xiàng)目了,我們使用 MysqlDataSource 吧,然后程序猿就會修改配置文件,重新加載配置文件,并重啟項(xiàng)目,完成數(shù)據(jù)源的切換。

          spring.datasource.type=com.mysql.cj.jdbc.MysqlDataSource

          在改變連接數(shù)據(jù)庫的數(shù)據(jù)源時(shí),只需要改變配置文件即可,無需改變?nèi)魏未a,原因是:

          • Spring Boot 底層封裝好了連接數(shù)據(jù)庫的數(shù)據(jù)源配置,利用反射,適配各個(gè)數(shù)據(jù)源。

          下面來簡略的進(jìn)行源碼分析。我們用ctrl+左鍵點(diǎn)擊spring.datasource.type進(jìn)入 DataSourceProperties 類中,發(fā)現(xiàn)使用setType() 將全類名轉(zhuǎn)化為 Class 對象注入到type成員變量當(dāng)中。在連接并監(jiān)控?cái)?shù)據(jù)庫時(shí),就會使用指定的數(shù)據(jù)源操作。

          private?Class?type;

          public?void?setType(Class?type)?{
          ????this.type?=?type;
          }

          Class對象指定了泛型上界DataSource,我們?nèi)タ匆幌赂鞔髷?shù)據(jù)源的類圖結(jié)構(gòu)


          上圖展示了一部分?jǐn)?shù)據(jù)源,當(dāng)然不止這些,但是我們可以看到,無論指定使用哪一種數(shù)據(jù)源,我們都只需要與配置文件打交道,而無需更改源碼,這就是反射的靈活性!

          破壞類的封裝性

          很明顯的一個(gè)特點(diǎn),反射可以獲取類中被private修飾的變量、方法和構(gòu)造器,這違反了面向?qū)ο蟮姆庋b特性,因?yàn)楸?private 修飾意味著不想對外暴露,只允許本類訪問,而setAccessable(true)可以無視訪問修飾符的限制,外界可以強(qiáng)制訪問。

          還記得單例模式一文嗎?里面講到反射破壞餓漢式和懶漢式單例模式,所以之后用了枚舉避免被反射KO。

          回到最初的起點(diǎn),SmallPineapple 里有一個(gè) weight 屬性被 private 修飾符修飾,目的在于自己的體重并不想給外界知道。

          public?class?SmallPineapple?{
          ????public?String?name;
          ????public?int?age;
          ????private?double?weight;?//?體重只有自己知道
          ????
          ????public?SmallPineapple(String?name,?int?age,?double?weight)?{
          ????????this.name?=?name;
          ????????this.age?=?age;
          ????????this.weight?=?weight;
          ????}
          ????
          }

          雖然 weight 屬性理論上只有自己知道,但是如果經(jīng)過反射,這個(gè)類就像在裸奔一樣,在反射面前變得一覽無遺

          SmallPineapple?sp?=?new?SmallPineapple("小菠蘿",?21,?"54.5");
          Clazz?clazz?=?Class.forName(sp.getClass());
          Field?weight?=?clazz.getDeclaredField("weight");
          weight.setAccessable(true);
          System.out.println("窺覷到小菠蘿的體重是:"?+?weight.get(sp));
          //?窺覷到小菠蘿的體重是:54.5 kg

          性能損耗

          在直接 new 對象并調(diào)用對象方法和訪問屬性時(shí),編譯器會在編譯期提前檢查可訪問性,如果嘗試進(jìn)行不正確的訪問,IDE會提前提示錯誤,例如參數(shù)傳遞類型不匹配,非法訪問 private 屬性和方法。

          而在利用反射操作對象時(shí),編譯器無法提前得知對象的類型,訪問是否合法,參數(shù)傳遞類型是否匹配。只有在程序運(yùn)行時(shí)調(diào)用反射的代碼時(shí)才會從頭開始檢查、調(diào)用、返回結(jié)果,JVM也無法對反射的代碼進(jìn)行優(yōu)化。

          雖然反射具有性能損耗的特點(diǎn),但是我們不能一概而論,產(chǎn)生了使用反射就會性能下降的思想,反射的慢,需要同時(shí)調(diào)用上100W次才可能體現(xiàn)出來,在幾次、幾十次的調(diào)用,并不能體現(xiàn)反射的性能低下。所以不要一味地戴有色眼鏡看反射,在單次調(diào)用反射的過程中,性能損耗可以忽略不計(jì)。如果程序的性能要求很高,那么盡量不要使用反射。

          反射基礎(chǔ)篇文末總結(jié)

          • 反射的思想:反射就像是一面鏡子一樣,在運(yùn)行時(shí)才看到自己是誰,可獲取到自己的信息,甚至實(shí)例化對象。
          • 反射的作用:在運(yùn)行時(shí)才確定實(shí)例化對象,使程序更加健壯,面對需求變更時(shí),可以最大程度地做到不修改程序源碼應(yīng)對不同的場景,實(shí)例化不同類型的對象。
          • 反射的應(yīng)用場景常見的有3個(gè):Spring的 IOC 容器,反射+工廠模式 使工廠類更穩(wěn)定,JDBC連接數(shù)據(jù)庫時(shí)加載驅(qū)動類
          • 反射的3個(gè)特點(diǎn):增加程序的靈活性、破壞類的封裝性以及性能損耗


          ? ? ? ?
          ???
          史上最全的延遲任務(wù)實(shí)現(xiàn)方式匯總!
          要被開除了,原因竟然是我不會 Java 注解
          丟棄掉那些 BeanUtils 工具類吧,MapStruct 真香!!!

          覺得不錯,點(diǎn)個(gè)在看~

          瀏覽 124
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  97精品人妻一区二区三区蜜桃 | 枕瑶钗十三回兴云弄雨又春风 | www.草逼| 亚洲无码视频在线观看观看 | 久久久久资源站 |