我是南城余!阿里云開發者平臺專家博士證書獲得者!
歡迎關注我的博客!一同成長!
一名從事運維開發的worker,記錄分享學習。
專注于AI,運維開發,windows Linux 系統領域的分享!
本章節對應知識庫
反射機制 · 語雀
反射
Java給我們提供的一套API,使用這套API可以在運行時動態獲取指定對象所屬的類,創建運行時類的對象,調用指定的結構(屬性、方法)等。
Reflection(反射)是被視為動態語言的關鍵,反射機制允許程序在運行期間借助于Reflection API取得任何類的內部信息,并能直接操作任意對象的內部屬性及方法。
面向對象,調用指定結構(屬性、方法)等功能,使用反射與不使用的區別
不使用反射,我們需要考慮封裝性。比如出了Person類之后,就不能調用Person類中私有的結構
使用反射,我們可以調用運行時類中任意的構造器、屬性、方法。包括了私有的屬性、方法、構造器。
反射與創建對象調用方法的方式使用場景
》從作為開發者角度,我們開發中主要是完成業務代碼,對于相關的對象、方法的調用都是確定的。所以在開發中,我們使用非反射的方式多一些。
》因為反射體現了動態性(可以在運行時動態的獲取對象所屬的類,動態的調用相關的方法),所以我們在涉及框架時,會使用大量的反射。意味著,如果需要學習框架源碼時,那么就需要學習反射。
框架 = 注解+反射+設計模式
封裝性:體現的是是否建議我們調用內部api的問題。比如,private聲明的結構,意味著不建議調用
反射: 體現的是我們能否調用的問題。因為類的完整結構都加載了內存中,所以我們就有能力進行調用
反射的優缺點
優點:
》提高了Java程序的靈活性和擴展性,降低了耦合性,提高了自適應能力
》允許程序創建個控制任何類的對象,無需提前硬編碼目標類
缺點:
》反射的性能較低
反射機制主要應用在對靈活性和擴展性要求很高的系統框架上
》反射會模糊程序內部邏輯,可讀性較差
反射,平時開發中,我們使用的并不多。主要是在框架的底層使用
class - 反射的源頭
針對于編寫好的。java源文件進行編譯(使用javac.exe)會生成一個或多個.class字節碼文件。接著,我們使用java.exe命令對指定的.class文件進行解釋運行。在這個解釋運行的過程中,我們需要將.class字節碼文件加載(使用類的加載器)到內存中(存在方法區)。加載到內存中的.class文件對應的結構即為Class的一個實例。
比如:加載到內存中的Person類或String類,都作為Class的一個一個的實例
Class clazz1 = Person.class;
Class clazz1 =String.class;
class可以看作是反射的源頭
獲取Class實例的幾種方式
方式1:要求編譯期間已知類型
前提:若已知具體的類,通過類的class屬性獲取,該方法最為安全可靠,程序性能最高
實例:
Class clazz = String.class;
方式2:獲取對象的運行時類型
前提:已知某個類的實例,調用該實例的getClass()方法獲取Class對象
實例:
Class clazz = "www.atguigu.com".getClass();
方式3:可以獲取編譯期間未知的類型
前提:已知一個類的全類名,且該類在類路徑下,可通過Class類的靜態方法forName()獲取,可能拋出ClassNotFoundException
實例:
Class clazz = Class.forName("java.lang.String");
方式4:其他方式(不做要求)
前提:可以用系統類加載對象或自定義加載器對象加載指定路徑下的類型
實例:
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("類的全類名");
Class的實例指向結構
簡言,所有的Java類型
》class:外部類,成員(成員內部類,靜態內部類),局部內部類,匿名內部類
》interface:接口
》[]:數組
》enum:枚舉
》annotation:注解@interface
》primitive type :基本數據類型
》void
類的加載過程(了解)
過程1:類的裝載(loading)
將類的class文件讀入內存,并為之創建一個java.lang.Class對象。此過程由類加載器完成
過程2:鏈接(linking)
> 驗證(Verify):確保加載的類信息符合JVM規范,例如:以cafebabe開頭,沒有安全方面的問題。
> 準備(Prepare):正式為類變量(static)分配內存并設置類變量默認初始值的階段,這些內存都將在方法區中進行分配。
> 解析(Resolve):虛擬機常量池內的符號引用(常量名)替換為直接引用(地址)的過程。
過程3:初始化(initialization)
執行類構造器<clinit>()方法的過程。
類構造器<clinit>()方法是由編譯期自動收集類中所有類變量的賦值動作和靜態代碼塊中的語句合并產生的。
關于類的加載器(了解、JDK8版本為例)
作用:負責類的加載,并對應于一個Class的實例。
分類(分為兩種):
> BootstrapClassLoader:引導類加載器、啟動類加載器
> 使用C/C++語言編寫的,不能通過Java代碼獲取其實例
> 負責加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內容)
> 繼承于ClassLoader的類加載器
> ExtensionClassLoader:擴展類加載器
> 負責加載從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄 下加載類庫
> SystemClassLoader/ApplicationClassLoader:系統類加載器、應用程序類加載器
> 我們自定義的類,默認使用的類的加載器。
> 用戶自定義類的加載器
> 實現應用的隔離(同一個類在一個應用程序中可以加載多份);數據的加密。
以上的類的加載器是否存在繼承關系? No!
使用類的加載器獲取流,并讀取配置文件信息
/*
* 需求:通過ClassLoader加載指定的配置文件
* */
@Test
public void test3() throws IOException {Properties pros = new Properties();//通過類的加載器讀取的文件的默認的路徑為:當前module下的src下InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");pros.load(is);String name = pros.getProperty("name");String pwd = pros.getProperty("password");System.out.println(name + ":" +pwd);
}
反射的應用
1. 創建運行時類的對象
如何實現
通過Class的實例調用newInstance()方法即可
且需要滿足以下條件:
》要求運行時必須提供一個空參構造器
》要求提供的空參構造器的權限要足夠
JavaBean中要求給當前類提供一個公共的的空參的構造器。
作用:
>場景1:子類對象在實例化時,子類的構造器的首行默認調用父類空參構造器
>場景2:在反射中,經常用來創建運行時類的對象。那么我們要求各個運行時類都提供一個空參構造器,便于我們編寫創建運行時類對象的代碼。
2. 獲取運行時類的內部結構
》獲取運行時類的內部結構:所有屬性、所有方法、所有構造器
》獲取運行時類的內部結構:父類、接口、包、帶泛型的父類、父類的泛型等
3. 調用指定的結構:指定的屬性、方法、構造器
調用指定的屬性步驟
步驟1. 通過Class實例調用getDeclareField(String fieldName),獲取運行時類指定名的屬性
步驟2. setAccessible(true),確保此屬性是可以訪問的
步驟3. 通過Field類的實例調用get(Object obj)(獲取操作)
或set(Object obj,Object value)(設置的操作)進行操作
調用指定的方法步驟
步驟1. 通過Class實例調用getDeclareField(String methodName,Class ... args),獲取運行時類指定的方法
步驟2. setAccessible(true),確保此屬性是可以訪問的
步驟3. 通過Method實例invoke(Object obj,Object .. objs),即為對Method對應方法的調用
invoke()返回值即為Method對應方法的返回值
特別的:如果Method對應的方法的返回值類型為void,則invoke()返回值為null
調用指定的構造器步驟
步驟1. 通過Class的實例調用getDeclaredConstructor(Class ... args),獲取指定參數的構造器
步驟2. setAccessible(true):確保此構造器是可訪問的
步驟3. 通過Constructor實例調用newInstance(Object ... objs),返回一個運行時類的實例
4. 注解的使用
框架層面