Java動态性有:反射機制,動态編譯/代理,位元組碼操作。常見的是反射和位元組碼操作。
Java讓我們在運作時識别對象和類的資訊,主要有2種方式:一種是傳統的RTTI,它假定我們在編譯時已經知道了所有的類型資訊;另一種是反射機制,它允許我們在運作時發現和使用類的資訊。
類的生命周期
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCM581dvRWYoNHLwEzX5xCMx8FesU2cfdGLwATMfRHLGZkRGZkRfJ3bs92YskmNhVTYykVNQJVMRhXVEF1X0hXZ0xiNx8VZ6l2cssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0UTO3IDNzEDM2gjN0QTNx8CXzEjMxgTMwIzLcNXZnFWbp9CXvwVbvNmLvR3YxUjL0M3Lc9CX6MHc0RHaiojIsJye.png)
類加載初始化階段,必須對類進行初始化的情況:
1、使用new關鍵字執行個體化對象時、讀取或者設定一個類的靜态字段(除final常量)以及調用靜态方法的時候。
2、使用反射包(java.lang.reflect)的方法對類進行反射調用時,如果類還沒有被初始化,則需先進行初始化,這點對反射很重要。
3、當初始化一個類的時候,如果其父類還沒進行初始化則需先觸發其父類的初始化。
4、當Java虛拟機啟動時,使用者需要指定一個要執行的主類(包含main方法的類),虛拟機會先初始化這個主類
5、當使用JDK 1.7 的動态語言支援時,如果一個java.lang.invoke.MethodHandle 執行個體最後解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄對應類沒有初始化時。
不會被初始化的情況:
當通路靜态域,隻有真正聲明這個域的類菜會被初始化。
通過子類引用父類的靜态變量,不會導緻子類初始化。
通過數組定義引用類,不會觸發此類初始化
引用常量不會觸發此類的初始化。
Class對象
Class類的對象作用是運作時提供或獲得某個對象的類型資訊。
了解RTTI在Java中的工作原理,首先需要知道類型資訊在運作時是如何表示的,這是由Class對象來完成的,它包含了與類有關的資訊。Class對象就是用來建立所有“正常”對象的,Java使用Class對象來執行RTTI,即使你正在執行的是類似類型轉換這樣的操作。
每個類都會産生一個對應的Class對象,也就是儲存在.class檔案。
比如建立一個Shapes類,編譯Shapes類後就會建立其包含Shapes類相關類型資訊的Class對象,并儲存在Shapes.class位元組碼檔案中。
所有類都是在對其第一次使用時,動态加載到JVM的,當程式建立一個對類的靜态成員的引用時,就會加載這個類。Class對象僅在需要的時候才會加載,static初始化是在類加載時進行的。
類加載器首先會檢查這個類的Class對象是否已被加載過,如果尚未加載,預設的類加載器就會根據類名查找對應的.class檔案。
想在運作時使用類型資訊,必須擷取對象的Class對象的引用,使用功能Class.forName(“Base”)可以實作該目的,或者使用base.class。
注:使用.class來建立Class對象的引用時,不會自動初始化該Class對象,使用forName()會自動初始化該Class對象。為了使用類而做的準備工作一般有以下3個步驟:
加載:由類加載器完成,找到對應的位元組碼,建立一個Class對象
連結:驗證類中的位元組碼,為靜态域配置設定空間
初始化:如果該類有超類,則對其初始化,執行靜态初始化器和靜态初始化塊
擷取Class類的四種方式
1.調用運作時類本身的.class屬性
Class clazz = String.class;
2.通過運作時類的對象擷取
Person p = new Person();
Class clazz = p.getClass();
3.通過Class的靜态方法擷取:展現反射的動态性
Class clazz = Class.forName("com.util.xxx");
4.通過類的加載器
ClassLoader classLoader = this.getClass().getClassLoader();
Class clazz = classLoader.loadClass("com.util.xxx");
這是一個執行個體方法,需要一個ClassLoader對象來調用該方法,該方法将Class檔案加載到記憶體時,并不會執行類的初始化,直到這個類第一次使用時才進行初始化.該方法因為需要得到一個ClassLoader對象,是以可以根據需要指定使用哪個類加載器.
反射
Class類和java.lang.reflect類庫一起對反射進行了支援,該類庫包含Field、Method和Constructor類,這些類的對象由JVM在啟動時建立,以表示未知類裡對應的成員。這就就可以使用Contructor建立新的對象,用get()和set()方法擷取和修改類中與Field對象關聯的字段,用invoke()方法調用與Method對象關聯的方法。另外,還可以調用getFields()、getMethods()和getConstructors()等許多便利的方法,以傳回表示字段、方法、以及構造器對象的數組,這樣,對象資訊可以在運作時被完全确定下來,而在編譯時不需要知道關于類的任何事情。
通過反射與一個未知類型的對象打交道時,JVM隻是簡單地檢查這個對象,看它屬于哪個特定的類。是以,那個類的.class對于JVM來說必須是可擷取的,要麼在本地機器上,要麼從網絡擷取。是以對于RTTI和反射之間的真正差別隻在于:
RTTI:編譯器在編譯時打開和檢查.class檔案
反射:運作時打開和檢查.class檔案
instanceof 關鍵字與isInstance方法
if(obj instanceof Animal){
Animal animal= (Animal) obj;
}
//isInstance方法則是Class類中的一個Native方法
if(Animal.class.isInstance(obj)){
參考:https://blog.csdn.net/javazejian/article/details/70768369
動态編譯
就是運作時動态生成java代碼, JAVA 6.0引入了動态編譯機制。
• 動态編譯的應用場景
比如浏覽器端編寫java代碼,上傳伺服器編譯和運作。
伺服器動态加載某些類檔案進行編譯。
• 動态編譯的兩種做法:
通過Runtime調用javac,啟動新的程序去操作,這是一種模拟操作。
Runtime run = Runtime.getRuntime();
Process process = run.exec("javac -cp d:/myjava/ HelloWorld.java");
通過JavaCompiler動态編譯
public static int compileFile(String sourceFile){
// 動态編譯
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null,sourceFile);
System.out.println(result==0?" 編譯成功 ":" 編譯失敗 ");
return result;
}
第一個參數: 為java編譯器提供參數
第二個參數: 得到 Java 編譯器的輸出資訊
第三個參數: 接收編譯器的 錯誤資訊
第四個參數: 可變參數(是一個String數組)能傳入一個或多個 Java 源檔案
傳回值: 0表示編譯成功,非0表示編譯失敗
動态編譯運作好的類:
1、通過Runtime.getRuntime() 運作啟動新的程序運作
Process process = run.exec("java -cp d:/myjava HelloWorld");
// Process process = run.exec("java -cp "+dir+" "+classFile);
2、通過反射運作編譯好的類
public static void runJavaClassByReflect(String dir,String classFile) throws Exception{
try {
URL[] urls = new URL[] {new URL("file:/"+dir)};
URLClassLoader loader = new URLClassLoader(urls);
Class c = loader.loadClass(classFile);
//調用加載類的main方法
c.getMethod("main",String[].class).invoke(null, (Object)new String[]{});
} catch (Exception e) {
e.printStackTrace();
腳本引擎
JAVA腳本引擎是從JDK6.0之後添加的新功能。
腳本引擎介紹:
使得 Java 應用程式可以通過一套固定的接口與各種腳本引擎互動,進而達到在 Java 平台上調用各種腳本語言的目的。
Java 腳本 API 是連通 Java 平台和腳本語言的橋梁。
可以把一些複雜異變的業務邏輯交給腳本語言處理。
獲得腳本引擎對象
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");
Java 腳本 API 為開發者提供了如下功能:
擷取腳本程式輸入,通過腳本引擎運作腳本并傳回運作結果,這是最核心的接口。
Java可以使用各種不同的實作,進而通用的調用js、python等腳本。
– 使用Js腳本引擎:Rhino
https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
Rhino 是一種使用 Java 語言編寫的 JavaScript 的開源實作,現在被內建進入JDK 6.0。
– 通過腳本引擎的運作上下文在腳本和 Java 平台間交換資料。
– 通過 Java 應用程式調用腳本函數。
位元組碼操作
就是在運作時對位元組碼操作,可以讓我們實作如下功能:
動态生成新的類
動态改變某個類的結構(添加、删除、修改 新的屬性和方法)
位元組碼操作的優勢:比反射開銷小,性能高
常見的位元組碼類庫如下;
-BECL :是java classworking廣泛使用的一種架構,可以深入了解JVM彙編語言進行類的操作細節。
基于JVM底層指令操作,比較難學。哈哈
-ASM :輕量級的java位元組碼操作架構類庫,直接涉及JVM底層操作和指令
-CGLIB :是基于ASM的的實作,強大性能高
-Javassist :開源架構,較簡單。
支援類庫和bytecode來操作位元組碼,性能相對BECL,ASM性能低下,跟CGLIB差不多,很多開源架構都在使用它,常見。