學習Spring必學的Java基礎知識(1)----反射
是什麼?
1.Java語言允許通過程式化的方式間接對Class進行操作,Class檔案由類裝載器裝載後,在JVM中將形成一份描述Class結構的元資訊物件,通過該元資訊物件可以獲知Class的結構資訊:如建構函式、屬性和方法等。Java允許使用者藉由這個Class相關的元資訊物件間接呼叫Class物件的功能,這就為使用程式化方式操作Class物件開闢了途徑。
2.Java Reflaction in Action有這麼一句話,可以解釋。反射是執行中的程式檢查自己和軟體執行環境的能力,它可以根據它發現的進行改變。通俗的講就是反射可以在執行時根據指定的類名獲得類的資訊。
ps:. class 檔案是.java的編譯檔案。在系統中的jvm只認識.class的二進位制編碼檔案。
為什麼?
首先我們先明確兩個概念,靜態編譯和動態編譯。
靜態編譯:在編譯時確定型別,繫結物件,即通過。 動態編譯:執行時確定型別,繫結物件。動態編譯最大限度發揮了java的靈活性,體現了多 態的應用,有以降低類之間的藕合性。
我們可以明確的看出動態編譯的好處,而反射就是運用了動態編譯建立物件。
先從某個程式碼案例上來解釋
若是不用反射,它是這樣的
interface fruit{ public abstract void eat(); } class Apple implements fruit{ public void eat(){ System.out.println("Apple"); } } class Orange implements fruit{ public void eat(){ System.out.println("Orange"); } } // 構造工廠類 // 也就是說以後如果我們在新增其他的例項的時候只需要修改工廠類就行了 class Factory{ public static fruit getInstance(String fruitName){ fruit f=null; if("Apple".equals(fruitName)){ f=new Apple(); } if("Orange".equals(fruitName)){ f=new Orange(); } return f; } } class hello{ public static void main(String[] a){ fruit f=Factory.getInstance("Orange"); f.eat(); } }
可以發現,每當我們要新增一種新的水果的時候,我們將不得不改變Factory中的原始碼,而往往改變原有正確程式碼是一種十分危險的行為。而且隨著水果種類的增加,你會發現你的factory類會越來越臃腫,
不得不說這是一種十分--的做法。(初學者可能會問,我們為什麼不直接在main方法中new水果那,我們可能會需要getInstance方法做一些別的事情。。。所以不直接new);
而反射無疑是一種聰明的辦法,看程式碼。
interface fruit{ public abstract void eat(); } class Apple implements fruit{ public void eat(){ System.out.println("Apple"); } } class Orange implements fruit{ public void eat(){ System.out.println("Orange"); } } class Factory{ public static fruit getInstance(String ClassName){ fruit f=null; try{ f=(fruit)Class.forName(ClassName).newInstance(); }catch (Exception e) { e.printStackTrace(); } return f; } } class hello{ public static void main(String[] a){ fruit f=Factory.getInstance("Reflect.Apple"); if(f!=null){ f.eat(); } } }
在出現新品種水果的時候,你完全不用去修改原有程式碼。
從上面的案例中,我們可以清楚的體會到反射的優越性。
因此為了符合開放封閉原則,改動程式碼少,提出了反射。
怎麼辦:
package com.xxq.rest.java.demo;
public class PrivateCar {
private String color;
protected void drive() {
System.out.println("drive private car! the color is:" + color);
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
package com.xxq.rest.java.demo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RelectionDemo {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
PrivateCar privateCar2 = new PrivateCar();
privateCar2.setColor("藍色");
System.out.println("老的方式 :" );
privateCar2.drive();
System.out.println("反射的方式 :" );
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Class<?> loadClass = null;
try {
loadClass = contextClassLoader.loadClass("com.xxq.rest.java.demo.PrivateCar");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
PrivateCar privateCar = (PrivateCar) loadClass.newInstance();
Method method = loadClass.getMethod("setColor", String.class);
method.invoke(privateCar, "紅色");
privateCar.drive();
}
}
老的方式 :
drive private car! the color is:藍色
反射的方式 :
drive private car! the color is:紅色
解析:
傳統方式是直接呼叫目標類的方法,後面一種我們通過Java反射機制以一種更加通用的方式間接地操作目標類
這說明我們完全可以通過程式設計方式呼叫Class的各項功能,這和直接通過建構函式和方法呼叫類功能的效果是一致的,只不過前者是間接呼叫,後者是直接呼叫罷了。 在ReflectTest中,使用了幾個重要的反射類,分別是ClassLoader、Class、Constructor和Method,通過這些反射類就可以間接呼叫目標Class的各項功能了。
1.我們獲取當前執行緒的ClassLoader,然後通過指定的全限定類“com.baobaotao.beans.Car”裝載Car類對應的反射例項。
ps:限定類名,就是類名全稱,帶包路徑的用點隔開,例如: java.lang.String
2.我們通過Car的反射類物件獲取Car的建構函式物件cons,通過建構函式物件的newInstrance()方法例項化Car物件,其效果等同於new Car();
3.我們又通過Car的反射類物件的getMethod(String methodName,Class paramClass)獲取屬性的Setter方法物件,第一個引數是目標Class的方法名;第二個引數是方法入參的物件型別。獲取方法反射物件後,即可通過invoke(Object obj,Object param)方法呼叫目標類的方法,該方法的第一個引數是操作的目標類物件例項;第二個引數是目標方法的入參。
類裝載器ClassLoader
1.類裝載器工作機制
1.1 類裝載器就是尋找類的節碼檔案並構造出類在JVM內部表示物件的元件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟:
[1.]裝載:查詢和匯入Class檔案; [2.]連結:執行校驗、準備和解析步驟,其中解析步驟是可以選擇的: [2.1]校驗:檢查載入Class檔案資料的正確性; [2.2]準備:給類的靜態變數分配儲存空間; [2.3]解析:將符號引用轉成直接引用; [3.]初始化:對類的靜態變數、靜態程式碼塊執行初始化工作。
1.2 類裝載工作由ClassLoader及其子類負責,ClassLoader是一個重要的Java執行時系統元件,它負責在執行時查詢和裝入Class位元組碼檔案。JVM在執行時會產生三個ClassLoader:根裝載器、ExtClassLoader(擴充套件類裝載器)和AppClassLoader(系統類裝載器)。
其中,根裝載器不是ClassLoader的子類,它使用C++編寫,因此我們在Java中看不到它,根裝載器負責裝載JRE的核心類庫,如JRE目標下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子類。其中ExtClassLoader負責裝載JRE擴充套件目錄ext中的JAR類包;AppClassLoader負責裝載Classpath路徑下的類包。
這三個類裝載器之間存在父子層級關係,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器。預設情況下,使用AppClassLoader裝載應用程式的類;
eg:
package com.xxq.rest.java.demo;
import java.lang.reflect.InvocationTargetException;
public class RelectionDemo {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:"+loader);
System.out.println("parent loader:"+loader.getParent());
System.out.println("grandparent loader:"+loader.getParent(). getParent());
}
}
current loader:[email protected]
parent loader:[email protected]
grandparent loader:null
我們知道當前的ClassLoader是AppClassLoader,父ClassLoader是ExtClassLoader,祖父ClassLoader是根類裝載器,因為在Java中無法獲得它的控制代碼,所以僅返回null。
JVM裝載類時使用“全盤負責委託機制”,“全盤負責”是指當一個ClassLoader裝載一個類的時,除非顯式地使用另一個ClassLoader,該類所依賴及引用的類也由這個ClassLoader載入;“委託機制”是指先委託父裝載器尋找目標類,只有在找不到的情況下才從自己的類路徑中查詢並裝載目標類。這一點是從安全形度考慮的,試想如果有人編寫了一個惡意的基礎類(如java.lang.String)並裝載到JVM中將會引起多麼可怕的後果。但是由於有了“全盤負責委託機制”,java.lang.String永遠是由根裝載器來裝載的,這樣就避免了上述事件的發生。
1.ClassLoader重要方法
在Java中,ClassLoader是一個抽象類,位於java.lang包中。下面對該類的一些重要介面方法進行介紹:
- Class loadClass(String name)
- name引數指定類裝載器需要裝載類的名字,必須使用全限定類名,如com.baobaotao. beans.Car。該方法有一個過載方法loadClass(String name ,boolean resolve),resolve引數告訴類裝載器是否需要解析該類。在初始化類之前,應考慮進行類解析的工作,但並不是所有的類都需要解析,如果JVM只需要知道該類是否存在或找出該類的超類,那麼就不需要進行解析。
- Class defineClass(String name, byte[] b, int off, int len)
- 將類檔案的位元組陣列轉換成JVM內部的java.lang.Class物件。位元組陣列可以從本地檔案系統、遠端網路獲取。name為位元組陣列對應的全限定類名。
- Class findSystemClass(String name)
- 從本地檔案系統載入Class檔案,如果本地檔案系統不存在該Class檔案,將丟擲ClassNotFoundException異常。該方法是JVM預設使用的裝載機制。
- Class findLoadedClass(String name)
- 呼叫該方法來檢視ClassLoader是否已裝入某個類。如果已裝入,那麼返回java.lang.Class物件,否則返回null。如果強行裝載已存在的類,將會丟擲連結錯誤。
- ClassLoader getParent()
- 獲取類裝載器的父裝載器,除根裝載器外,所有的類裝載器都有且僅有一個父裝載器,ExtClassLoader的父裝載器是根裝載器,因為根裝載器非Java編寫,所以無法獲得,將返回null。
除JVM預設的三個ClassLoader以外,可以編寫自己的第三方類裝載器,以實現一些特殊的需求。類檔案被裝載並解析後,在JVM內將擁有一個對應的java.lang.Class類描述物件,該類的例項都擁有指向這個類描述物件的引用,而類描述物件又擁有指向關聯ClassLoader的引用,如圖3-4所示。
每一個類在JVM中都擁有一個對應的java.lang.Class物件,它提供了類結構資訊的描述。陣列、列舉、註解以及基本Java型別(如int、double等),甚至void都擁有對應的Class物件。Class沒有public的構造方法。Class物件是在裝載類時由JVM通過呼叫類裝載器中的defineClass()方法自動構造的。
2.Java反射機制
Class反射物件描述類語義結構,可以從Class物件中獲取建構函式、成員變數、方法類等類元素的反射物件,並以程式設計的方式通過這些反射物件對目標類物件進行操作。
這些反射物件類在java.reflect包中定義,下面是最主要的三個反射類:
- Constructor:類的建構函式反射類,通過Class#getConstructors()方法可以獲得類的所有建構函式反射物件陣列。在JDK5.0中,還可以通過getConstructor(Class... parameterTypes)獲取擁有特定入參的建構函式反射物件。Constructor的一個主要方法是newInstance(Object[] initargs),通過該方法可以建立一個物件類的例項,相當於new關鍵字。在JDK5.0中該方法演化為更為靈活的形式:newInstance (Object... initargs)。
- Method:類方法的反射類,通過Class#getDeclaredMethods()方法可以獲取類的所有方法反射類物件陣列Method[]。在JDK5.0中可以通過getDeclaredMethod(String name, Class... parameterTypes)獲取特定簽名的方法,name為方法名;Class...為方法入參型別列表。Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目標物件;args為方法入參,程式碼清單3 10③處演示了這個反射類的使用方法。在JDK 5.0中,該方法的形式調整為invoke(Object obj, Object... args)。此外,Method還有很多用於獲取類方法更多資訊的方法:
- 1)Class getReturnType():獲取方法的返回值型別; 2)Class[] getParameterTypes():獲取方法的入參型別陣列; 3)Class[] getExceptionTypes():獲取方法的異常型別陣列; 4)Annotation[][] getParameterAnnotations():獲取方法的註解資訊,JDK 5.0中的新方法;
- Field:類的成員變數的反射類,通過Class#getDeclaredFields()方法可以獲取類的成員變數反射物件陣列,通過Class#getDeclaredField(String name)則可獲取某個特定名稱的成員變數反射物件。Field類最主要的方法是set(Object obj, Object value),obj表示操作的目標物件,通過value為目標物件的成員變數設定值。如果成員變數為基礎型別,使用者可以使用Field類中提供的帶型別名的值設定方法,如setBoolean(Object obj, boolean value)、setInt(Object obj, int value)等。
此外,Java還為包提供了Package反射類,在JDK 5.0中還為註解提供了AnnotatedElement反射類。總之,Java的反射體系保證了可以通過程式化的方式訪問目標類中所有的元素,對於private或protected的成員變數和方法,只要JVM的安全機制允許,也可以通過反射進行呼叫,請看下面的例子:
參考檔案: