Java基礎 -- 深刻理解Java類型信息(Class對象)與反射機制

一 RTTI概念

認識Class對象以前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RTTI的說法則是源於《Thinking in Java》一書,其做用是在運行時識別一個對象的類型和類的信息,這裏分兩種:html

  • 傳統的」RTTI」:它假定咱們在編譯期已知道了全部類型(在沒有反射機制建立和使用類對象時,通常都是編譯期已肯定其類型,如new對象時該類必須已定義好);
  • 反射機制,它容許咱們在運行時發現和使用類型的信息。

二 Class對象

在Java中用來表示運行時類型信息的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分源碼以下:java

public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static native void registerNatives();
    static {
        registerNatives();
    }

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.(私有構造,只能由JVM建立該類)
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

Class類被建立後的對象就是Class對象,注意,Class對象表示的是本身手動編寫類的類型信息,好比建立一個Shapes類,那麼,JVM就會建立一個Shapes對應Class類的Class對象,該Class對象保存了Shapes類相關的類型信息。程序員

實際上在Java中每一個類都有一個Class對象,每當咱們編寫而且編譯一個新建立的類就會產生一個對應Class對象而且這個Class對象會被保存在同名.class文件裏(編譯後的字節碼文件保存的就是Class對象),那爲何須要這樣一個Class對象呢?是這樣的,當咱們new一個新對象或者引用靜態成員變量時,Java虛擬機(JVM)中的類加載器子系統會將對應Class對象加載到JVM中,而後JVM再根據這個類型信息相關的Class對象建立咱們須要實例對象或者提供靜態變量的引用值。須要特別注意的是,手動編寫的每一個class類,不管建立多少個實例對象,在JVM中都只有一個Class對象,即在內存中每一個類有且只有一個相對應的Class對象,挺拗口,經過下圖理解(內存中的簡易現象圖):
編程

到這咱們也就能夠得出如下幾點信息:數組

  • Class類也是類的一種,與class關鍵字是不同的;
  • 手動編寫的類被編譯後會產生一個Class對象,其表示的是建立的類的類型信息,並且這個Class對象保存在同名.class的文件中(字節碼文件),好比建立一個Shapes類,編譯Shapes類後就會建立其包含Shapes類相關類型信息的Class對象,並保存在Shapes.class字節碼文件中;
  • 每一個經過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,不管建立多少個實例對象,其依據的都是用一個Class對象;
  • Class類只存私有構造函數,所以對應Class對象只能有JVM建立和加載;
  • Class類的對象做用是運行時提供或得到某個對象的類型信息,這點對於反射技術很重要(關於反射稍後分析)。

一、Class對象的加載

前面咱們已提到過,Class對象是由JVM加載的,那麼其加載時機是?安全

實際上全部的類都是在對其第一次使用時動態加載到JVM中的,當程序第一次調用類的靜態成員時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),注意,使用new操做符建立類的新實例對象也會被看成調用類的靜態成員(構造函數也是類的靜態方法),由此看來Java程序在它們開始運行以前並不是被徹底加載到內存的,其各個部分是按需加載,因此在使用該類時,類加載器首先會檢查這個類的Class對象是否已被加載(類的實例對象建立時依據Class對象中類型信息完成的),若是尚未加載,默認的類加載器就會先根據類名查找.class文件(編譯後Class對象被保存在同名的.class文件中),在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞而且不包含不良Java代碼(這是java的安全機制檢測),徹底沒有問題後就會被動態加載到內存中,此時至關於Class對象也就被載入內存了(畢竟.class字節碼文件保存的就是Class對象),同時Class對象也就能夠被用來建立這個類的全部實例對象框架

下面經過一個簡單例子來講明Class對象被加載的時機問題(例子引用自Thinking in Java):dom

class Candy {
     static { 
         System.out.println("Loading Candy"); 
      }
}

class Gum {
    static { 
        System.out.println("Loading Gum"); 
    }
}

class Cookie {
    static {   
        System.out.println("Loading Cookie"); 
    }
}

public class SweetShop {
    
    public static void print(Object obj) {
        System.out.println(obj);
    }
    
    public static void main(String[] args) {  
        print("inside main");
        new Candy();
        print("After creating Candy");
        try {
            Class.forName("Gum");
        } catch(ClassNotFoundException e) {
            print("Couldn't find Gum");
        }
        print("After Class.forName(\"Gum\")");
        new Cookie();
        print("After creating Cookie");
  }
    
}

在上述代碼中,每一個類Candy、Gum、Cookie都存在一個static語句(static初始化是在類加載時進行的),這個語句會在類第一次被加載時執行,這個語句的做用就是告訴咱們該類在何時被加載,執行結果:編程語言

inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

從結果來看,new一個Candy對象和Cookie對象,構造函數將被調用,屬於靜態方法的引用,Candy類的Class對象和Cookie的Class對象確定會被加載,畢竟Candy實例對象的建立依據其Class對象。比較有意思的是:ide

Class.forName("Gum");

其中forName方法是Class類(全部Class對象都屬於這個類)的一個static成員方法。Class對象和其餘對象同樣,咱們能夠獲取並操做它的引用(這就是類加載器的工做)。這裏經過forName方法,咱們能夠獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會致使Gum類被加載(前提是Gum類歷來沒有被加載過)。

二、Class.forName()方法

經過上述的案例,咱們知道Class.forName()方法的調用將會返回一個對應類的Class對象,所以若是咱們想獲取一個類的運行時類型信息並加以使用時,能夠調用Class.forName()方法獲取該類對應的Class對象的引用,這樣作的好處是無需經過持有該類的實例對象引用而去獲取Class對象,以下的第2種方式是經過一個實例對象獲取一個類的Class對象,其中的getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。

public static void main(String[] args) {

    try{
        //經過Class.forName獲取Gum類的Class對象
        Class clazz=Class.forName("Gum");
        System.out.println("forName=clazz:"+clazz.getName());
    }catch (ClassNotFoundException e){
        e.printStackTrace();
    }

    //經過實例對象獲取Gum的Class對象
    Gum gum = new Gum();
    Class clazz2 = gum.getClass();
    System.out.println("new=clazz2:"+clazz2.getName());
}

 注意調用forName方法時須要捕獲一個名稱爲ClassNotFoundException的異常,由於forName方法在編譯器是沒法檢測到其傳遞的字符串對應的類是否存在的,只能在程序運行時進行檢查,若是不存在就會拋出ClassNotFoundException異常。

三、Class字面常量

在Java中存在另外一種方式來生成Class對象的引用,它就是Class字面常量,以下:

//字面常量的方式獲取Class對象
Class clazz = Gum.class;

這種方式相對前面兩種方法更加簡單,更安全。由於它在編譯器就會受到編譯器的檢查同時因爲無需調用forName方法效率也會更高,由於經過字面量的方法獲取Class對象的引用不會自動初始化該類。更加有趣的是字面常量的獲取Class對象引用方式不只能夠應用於普通的類,也能夠應用用接口,數組以及基本數據類型,這點在反射技術應用傳遞參數時頗有幫助,關於反射技術稍後會分析,因爲基本數據類型還有對應的基本包裝類型,其包裝類型有一個標準字段TYPE,而這個TYPE就是一個引用,指向基本數據類型的Class對象,其等價轉換以下,通常狀況下更傾向使用.class的形式,這樣能夠保持與普通類的形式統一。

boolean.class = Boolean.TYPE;
char.class = Character.TYPE;
byte.class = Byte.TYPE;
short.class = Short.TYPE;
int.class = Integer.TYPE;
long.class = Long.TYPE;
float.class = Float.TYPE;
double.class = Double.TYPE;
void.class = Void.TYPE;
上面的=表示等價的意思

前面提到過,使用字面常量的方式獲取Class對象的引用不會觸發類的初始化,這裏咱們可能須要簡單瞭解一下類加載的過程,以下:

  • 加載:類加載過程的一個階段:經過一個類的徹底限定查找此類字節碼文件,並利用字節碼文件建立一個Class對象;
  • 連接:驗證字節碼的安全性和完整性,準備階段正式爲靜態域分配存儲空間,注意此時只是分配靜態成員變量的存儲空間,不包含實例成員變量,若是必要的話,解析這個類建立的對其餘類的全部引用;
  • 初始化:類加載最後階段,若該類具備超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量。

由此可知,咱們獲取字面常量的Class引用時,觸發的應該是加載階段,由於在這個階段Class對象已建立完成,獲取其引用並不困難,而無需觸發類的最後階段初始化。下面經過小例子來驗證這個過程:

import java.util.*;

class Initable {
      //編譯期靜態常量
      static final int staticFinal = 47;
      //非編期靜態常量
      static final int staticFinal2 =
      ClassInitialization.rand.nextInt(1000);
      static {
            System.out.println("Initializing Initable");
      }
}

class Initable2 {
      //靜態成員變量
      static int staticNonFinal = 147;
      static {
          System.out.println("Initializing Initable2");
      }
}

class Initable3 {
      //靜態成員變量
      static int staticNonFinal = 74;
      static {
          System.out.println("Initializing Initable3");
      }
}

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
        //字面常量獲取方式獲取Class對象
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        //不觸發類初始化
        System.out.println(Initable.staticFinal);
        //會觸發類初始化
        System.out.println(Initable.staticFinal2);
        //會觸發類初始化
        System.out.println(Initable2.staticNonFinal);
        //forName方法獲取Class對象
        Class initable3 = Class.forName("Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}

 執行結果:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

從輸出結果來看,能夠發現,經過字面常量獲取方式獲取Initable類的Class對象並無觸發Initable類的初始化,這點也驗證了前面的分析,。

同時發現調用Initable.staticFinal變量時也沒有觸發初始化,這是由於staticFinal屬於編譯期靜態常量,在編譯階段經過常量傳播優化的方式將Initable類的常量staticFinal存儲到了一個稱爲NotInitialization類的常量池中,在之後對Initable類常量staticFinal的引用實際都轉化爲對NotInitialization類對自身常量池的引用,因此在編譯期後,對編譯期常量的引用都將在NotInitialization類的常量池獲取,這也就是引用編譯期靜態常量不會觸發Initable類初始化的重要緣由。

但在以後調用了Initable.staticFinal2變量後就觸發了Initable類的初始化,注意staticFinal2雖然被static和final修飾,但其值在編譯期並不能肯定,所以staticFinal2並非編譯期常量,使用該變量必須先初始化Initable類。

Initable2和Initable3類中都是靜態成員變量並不是編譯期常量,引用都會觸發初始化。

至於forName方法獲取Class對象,確定會觸發初始化,這點在前面已分析過。到這幾種獲取Class對象的方式也都分析完,到此這裏能夠得出小結論:

  • 獲取Class對象引用的方式3種,經過繼承自Object類的getClass()方法,Class類的靜態方法forName()以及字面常量的方式」.class」;
  • 其中實例類的getClass()方法和Class類的靜態方法forName()都將會觸發類的初始化階段,而字面常量獲取Class對象的方式則不會觸發初始化;
  • 初始化是類加載的最後一個階段,也就是說完成這個階段後類也就加載到內存中(Class對象在加載階段已被建立),此時能夠對類進行各類必要的操做了(如new對象,調用靜態成員等),注意在這個階段,才真正開始執行類中定義的Java程序代碼或者字節碼。

關於類加載的初始化階段,在虛擬機規範嚴格規定了有且只有5種場景必須對類進行初始化:

  • 使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(不包含編譯期常量)以及調用靜態方法的時候,必須觸發類加載的初始化過程(類加載過程最終階段);
  • 使用反射包(java.lang.reflect)的方法對類進行反射調用時,若是類尚未被初始化,則需先進行初始化,這點對反射很重要;
  • 當初始化一個類的時候,若是其父類還沒進行初始化則需先觸發其父類的初始化;
  • 當Java虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類;
  • 當使用JDK 1.7 的動態語言支持時,若是一個java.lang.invoke.MethodHandle 實例最後解析結果爲REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應類沒有初始化時,必須觸發其初始化(這點看不懂就算了,這是1.7的新增的動態語言支持,其關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期進行的,這是一個比較大點的話題,這裏暫且打住)。

四、理解泛化的Class對象引用

因爲Class的引用老是指向某個類的Class對象,利用Class對象能夠建立實例類,這也就足以說明Class對象的引用指向的對象確切的類型。在Java SE5引入泛型後,咱們能夠利用泛型來表示Class對象更具體的類型,即便在運行期間會被擦除,但編譯期足以確保咱們使用正確的對象類型。以下:

public class ClazzDemo {

    public static void main(String[] args){
        //沒有泛型
        Class intClass = int.class;

        //帶泛型的Class對象
        Class<Integer> integerClass = int.class;

        integerClass = Integer.class;

        //沒有泛型的約束,能夠隨意賦值
        intClass= double.class;

        //編譯期錯誤,沒法編譯經過
        //integerClass = double.class
    }
}

從代碼能夠看出,聲明普通的Class對象,在編譯器並不會檢查Class對象的確切類型是否符合要求,若是存在錯誤只有在運行時才得以暴露出來。可是經過泛型聲明指明類型的Class對象,編譯器在編譯期將對帶泛型的類進行額外的類型檢查,確保在編譯期就能保證類型的正確性,實際上Integer.class就是一個Class<Integer>類的對象。面對下述語句,確實可能使人困惑,但該語句確實是沒法編譯經過的。

//編譯沒法經過
Class<Number> numberClass=Integer.class;

咱們或許會想Integer不就是Number的子類嗎?然而事實並不是這般簡單,畢竟Integer的Class對象並不是Number的Class對象的子類,前面提到過,全部的Class對象都只來源於Class類,看來事實確實如此。固然咱們能夠利用通配符「?」來解決問題:

Class<?> intClass = int.class;
intClass = double.class;

這樣的語句並無什麼問題,畢竟通配符指明全部類型都適用,那麼爲何不直接使用Class還要使用Class<?>呢?這樣作的好處是告訴編譯器,咱們是確實是採用任意類型的泛型,而非忘記使用泛型約束,所以Class<?>老是優於直接使用Class,至少前者在編譯器檢查時不會產生警告信息。固然咱們還可使用extends關鍵字告訴編譯器接收某個類型的子類,如解決前面Number與Integer的問題:

//編譯經過!
Class<? extends Number> clazz = Integer.class;
//賦予其餘類型
clazz = double.class;
clazz = Number.class;

上述的代碼是行得通的,extends關鍵字的做用是告訴編譯器,只要是Number的子類均可以賦值。這點與前面直接使用Class<Number>是不同的。實際上,應該時刻記住向Class引用添加泛型約束僅僅是爲了提供編譯期類型的檢查從而避免將錯誤延續到運行時期

下面的示例使用了泛型類語法。它存儲了一個類引用,稍後又產生了一個List,填充這個List的對象是使用newInstance()方法,經過該引用生成的:

import java.util.*;

class CountedInteger{
    private static long counter;
    //每次建立一個實例,都會執行一次初始化id=counter++
    private final long id = counter++;
    public String toString(){
        return Long.toString(id);
    }
}

public class FilledList<T>{
    private Class<T> type;
    public FilledList(Class<T> type){
        this.type = type;
    }
    public List<T> create(int nElements){
        List<T> result = new ArrayList<T>();
        try{
            for(int i=0;i<nElements;i++){
                //CountedInteger必須提供默認的構造函數,經過CountedInteger類的Class對象建立一個實例對象
                result.add(type.newInstance());
            }
        }catch(Exception e){
                throw new RuntimeException(e);
        }    
        return result;
    }
    
    public static void main(String[] args){
        FilledList<CountedInteger> f1 = new FilledList<CountedInteger>(CountedInteger.class);
        System.out.println(f1.create(15));
    }
}

輸出以下:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

五、關於類型轉換的問題

在Java SE5中新增一種使用Class對象進行類型轉換的方式,即cast()方式:

class Building{}
class House extends Building{}

public class ClassCasts {
    public static void main(String[] args) {
        Building b = new House();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        h = (House)b;
    }
}

利用Class對象的cast()方法,其參數接收一個參數對象並將其轉換爲Class引用的類型。固然,若是仔細觀察上面的代碼,則會發現,與實現了相同功能的main()中最後一行相比,這種轉型好像作了不少額外的工做。新的轉型語法對於沒法使用普通類型的狀況顯得很是有用,在編寫泛型代碼時,若是存儲了Class引用,並但願之後經過這個引用來執行轉型,這種狀況就會時有發生。

六、instanceof 關鍵字與isInstance方法

關於instanceof 關鍵字,它返回一個boolean類型的值,意在告訴咱們對象是否是某個特定的類型實例。以下,在強制轉換前利用instanceof檢測obj是否是Animal類型的實例對象,若是返回true再進行類型轉換,這樣能夠避免拋出類型轉換的異常(ClassCastException)

    public void cast(Object obj){
        if(obj instanceof Animal){
              Animal animal= (Animal) obj;
          }
    }

而isInstance方法則是Class類中的一個Native方法,也是用於判斷對象類型的,看個簡單例子:

    public void cast2(Object obj){
        //isInstance方法
        if(Animal.class.isInstance(obj)){
            Animal animal= (Animal) obj;
        }
    }

事實上instanceOf 與isInstance方法產生的結果是相同的。對於instanceof是關鍵字只被用於對象引用變量,檢查左邊對象是否是右邊類或接口的實例化。若是被測對象是null值,則測試結果老是false。通常形式:

//判斷這個對象是否是這種類型
obj instanceof className

而isInstance方法則是Class類的Native方法,其中obj是被測試的對象或者變量,若是obj是調用這個方法的class或接口的實例,則返回true。若是被檢測的對象是null或者基本類型,那麼返回值是false;通常形式以下:

//判斷這個對象能不能被轉化爲這個類
className.class.inInstance(obj)

最後這裏給出一個簡單實例,驗證isInstance方法與instanceof等價性:

class A {}

class B extends A {}

public class Instance {
    
    public static void print(String msg) {
        System.out.println(msg);
    }
    
    public static void test(Object x) {
        print("Testing x of type: " + x.getClass());
        print("x instanceof A: " + (x instanceof A));
        print("x instanceof B: "+ (x instanceof B));
        print("A.isInstance(x): "+ A.class.isInstance(x));
        print("B.isInstance(x): " + B.class.isInstance(x));
        print("x.getClass() == A.class " + (x.getClass() == A.class));
        print("x.getClass() == B.class " + (x.getClass() == B.class));
        print("x.getClass().equals(A.class)) "+ (x.getClass().equals(A.class)));
        print("x.getClass().equals(B.class)) " + (x.getClass().equals(B.class)));
    }
    
    public static void main(String[] args) {
        test(new A());
        test(new B());
    } 
}

輸出以下:

Testing x of type: class A
x instanceof A: true
x instanceof B: false
A.isInstance(x): true
B.isInstance(x): false
x.getClass() == A.class true
x.getClass() == B.class false
x.getClass().equals(A.class)) true
x.getClass().equals(B.class)) false
Testing x of type: class B
x instanceof A: true
x instanceof B: true
A.isInstance(x): true
B.isInstance(x): true
x.getClass() == A.class false
x.getClass() == B.class true
x.getClass().equals(A.class)) false
x.getClass().equals(B.class)) true

到此關於Class對象相關的知識點都分析完了,下面將結合Class對象的知識點分析反射技術。

三 反射技術

反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性,這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。一直以來反射技術都是Java中的閃亮點,這也是目前大部分框架(如Spring/Mybatis等)得以實現的支柱。在Java中,Class類與java.lang.reflect類庫一塊兒對反射技術進行了全力的支持。在反射包中,咱們經常使用的類主要有Constructor類表示的是Class 對象所表示的類的構造方法,利用它能夠在運行時動態建立對象、Field表示Class對象所表示的類的成員變量,經過它能夠在運行時動態修改爲員變量的屬性值(包含private)、Method表示Class對象所表示的類的成員方法,經過它能夠動態調用對象的方法(包含private),下面將對這幾個重要類進行分別說明。

一、Constructor類及其用法

Constructor類存在於反射包(java.lang.reflect)中,反映的是Class 對象所表示的類的構造方法。獲取Constructor對象是經過Class類中的方法獲取的,Class類與Constructor相關的主要方法以下:

方法返回值 方法名稱 方法說明
static Class<?> forName(String className) 返回與帶有給定字符串名的類或接口相關聯的 Class 對象
Constructor<T> getConstructor(Class<?>... parameterTypes) 返回指定參數類型、具備public訪問權限的構造函數對象
Constructor<?>[] getConstructors() 返回全部具備public訪問權限的構造函數的Constructor對象數組
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回指定參數類型、全部聲明的(包括private)構造函數對象
Constructor<?>[] getDeclaredConstructor() 返回全部聲明的(包括private)構造函數對象
T newInstance() 建立此 Class 對象所表示的類的一個新實例。

 

下面看一個簡單例子來了解Constructor對象的使用:

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
class User { private int age; private String name; //無參數構造 public User() { super(); } public void setName(String name) { // TODO Auto-generated method stub this.name = name; } public void setAge(int i) { // TODO Auto-generated method stub this.age = i; } //一個參數構造 public User(String name) { super(); this.name = name; } /** * 私有構造 * @param age * @param name */ @SuppressWarnings("unused") private User(int age, String name) { super(); this.age = age; this.name = name; } @Override public String toString() { // TODO Auto-generated method stub return "My name is " + name + ", age is " + age; } } @SuppressWarnings("serial") public class ReflectDemo implements Serializable{ public static void main(String[] args) throws Exception { Class<?> clazz = null; //獲取User類對應的Class對象的引用 clazz = Class.forName("User"); //第一種方法,實例化默認構造方法,User必須有無參構造函數,不然將拋異常 User user = (User) clazz.newInstance(); user.setAge(20); user.setName("Rollen"); System.out.println("user:" + user); System.out.println("--------------------------------------------"); //獲取帶String參數的public構造函數 Constructor cs1 = clazz.getConstructor(String.class); //建立User User user1= (User) cs1.newInstance("Tom"); user1.setAge(22); System.out.println("user1:" + user1); System.out.println("--------------------------------------------"); //取得指定帶int和String參數構造函數,該方法是私有構造private Constructor cs2 = clazz.getDeclaredConstructor(int.class,String.class); //因爲是private必須設置可訪問 cs2.setAccessible(true); //建立user對象 User user2= (User) cs2.newInstance(25,"Like"); System.out.println("user2:"+user2); System.out.println("--------------------------------------------"); //獲取全部構造包含private Constructor<?>[] cons = clazz.getDeclaredConstructors(); //查看每一個構造方法須要的參數 for (int i = 0; i < cons.length; i++) { //獲取構造函數參數類型 Class<?> clazzs[] = cons[i].getParameterTypes(); System.out.println("構造函數["+i+"]:"+cons[i].toString() ); System.out.print("參數類型["+i+"]:("); for (int j = 0; j < clazzs.length; j++) { if (j == clazzs.length - 1) System.out.print(clazzs[j].getName()); else System.out.print(clazzs[j].getName() + ","); } System.out.println(")"); } } }

輸出以下:

user:My name is Rollen, age is 20
--------------------------------------------
user1:My name is Tom, age is 22
--------------------------------------------
user2:My name is Like, age is 25
--------------------------------------------
構造函數[0]:private User(int,java.lang.String)
參數類型[0]:(int,java.lang.String)
構造函數[1]:public User(java.lang.String)
參數類型[1]:(java.lang.String)
構造函數[2]:public User()
參數類型[2]:()

 關於Constructor類自己一些經常使用方法以下(僅部分,其餘可查API),

方法返回值 方法名稱 方法說明
Class<T> getDeclaringClass() 返回 Class 對象,該對象表示聲明由此 Constructor 對象表示的構造方法的類,其實就是返回真實類型(不包含參數)。
Type[] getGenericParameterTypes() 按照聲明順序返回一組 Type 對象,返回的就是 Constructor對象構造函數的形參類型。
String getName() 以字符串形式返回此構造方法的名稱。
Class<?>[] getParameterTypes() 按照聲明順序返回一組 Class 對象,即返回Constructor 對象所表示構造方法的形參類型。
T newInstance(Object... initargs) 使用此 Constructor對象表示的構造函數來建立新實例。
String toGenericString() 返回描述此 Constructor 的字符串,其中包括類型參數。

在上面程序追加以下內容:

        System.out.println("--------------------------------------------");
        //Constructor類自己一些經常使用方法
        System.out.println("------getDeclaredClass----------");
        Class uclazz = cs2.getDeclaringClass();            
        //Constructor對象表示的構造方法的類
        System.out.println("構造方法的類:" + uclazz.getName());
        
        System.out.println("------getGenericParameterTypes--------");
        //按照聲明順序返回一組 Type 對象,返回的就是 Constructor對象構造函數的形參類型
        Type[] tps = cs2.getGenericParameterTypes();
        for(Type tp:tps) {
            System.out.println("參數名稱tp:" + tp);
        }
        
        System.out.println("------getParameterTypes--------");
        //按照聲明順序返回一組 Class 對象,返回的就是 Constructor對象構造函數的形參類型
        Class[] clazzs = cs2.getParameterTypes();
        for(Class claz:clazzs) {
            System.out.println("參數名稱tp:" + claz.getName());
        } 
        
        System.out.println("------getName-------");
        //以字符串形式返回此構造方法的名稱
        System.out.println("getName:" + cs2.getName());
        
        
        System.out.println("------toGenericString-------");
        //返回描述此 Constructor 的字符串,其中包括類型參數。
        System.out.println("toGenericString:" + cs2.toGenericString());

輸出以下:

--------------------------------------------
------getDeclaredClass----------
構造方法的類:User
------getGenericParameterTypes--------
參數名稱tp:int
參數名稱tp:class java.lang.String
------getParameterTypes--------
參數名稱tp:int
參數名稱tp:java.lang.String
------getName-------
getName:User
------toGenericString-------
toGenericString:private User(int,java.lang.String)

其中關於Type類型這裏簡單說明一下,Type 是 Java 編程語言中全部類型的公共高級接口。它們包括原始類型、參數化類型、數組類型、類型變量和基本類型。getGenericParameterTypes 與 getParameterTypes 都是獲取構成函數的參數類型,前者返回的是Type類型,後者返回的是Class類型,因爲Type是頂級接口,Class也實現了該接口,所以Class類是Type的子類,Type 表示的所有類型而每一個Class對象表示一個具體類型的實例,如String.class僅表明String類型。由此看來Type與 Class 表示類型幾乎是相同的,只不過 Type表示的範圍比Class要廣得多而已。固然Type還有其餘子類,如:

  • TypeVariable:表示類型參數,能夠有上界,好比:T extends Number;
  • ParameterizedType:表示參數化的類型,有原始類型和具體的類型參數,好比:List<String>;
  • WildcardType:表示通配符類型,好比:?, ? extends Number, ? super Integer;

經過以上的分析,對於Constructor類已有比較清晰的理解,利用好Class類和Constructor類,咱們能夠在運行時動態建立任意對象,從而突破必須在編譯期知道確切類型的障礙。

二、Field類及其用法

Field 提供有關類或接口的單個字段的信息,以及對它的動態訪問權限。反射的字段多是一個類(靜態)字段或實例字段。一樣的道理,咱們能夠經過Class類的提供的方法來獲取表明字段信息的Field對象,Class類與Field對象相關方法以下:

方法返回值 方法名稱 方法說明
Field getDeclaredField(String name) 獲取指定name名稱的(包含private修飾的)字段,不包括繼承的字段
Field[] getDeclaredFields() 獲取Class對象所表示的類或接口的全部(包含private修飾的)字段,不包括繼承的字段
Field getField(String name) 獲取指定name名稱、具備public修飾的字段,包含繼承字段
Field[] getFields() 獲取修飾符爲public的字段,包含繼承字段

下面的代碼演示了上述方法的使用過程:

import java.lang.reflect.*;

class Person{
    public int age;
    public String name;
}

class Student extends Person{
    public String desc;
    private int score;
}


public class ReflectField {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException {
        Class<?> clazz = Class.forName("Student");
        
        //獲取指定name名稱、具備public修飾的字段,包含繼承字段
        Field field = clazz.getField("age");
        System.out.println("field:" + field);
        System.out.println("-----------------------");
        
        //獲取修飾符爲public的字段,包含繼承字段
        Field[] fields = clazz.getFields();
        for(Field f:fields) {
            System.out.println("field:" + f);
        }
        System.out.println("-----------------------");
        
        
        //獲取指定name名稱的(包含private修飾的)字段,不包括繼承的字段
        Field field2 = clazz.getDeclaredField("desc");
        System.out.println("field2:" + field2);
        System.out.println("-----------------------");
        
        //獲取Class對象所表示的類或接口的全部(包含private修飾的)字段,不包括繼承的字段
        Field[] fields2 = clazz.getDeclaredFields();
        for(Field f:fields2) {
            System.out.println("field:" + f);
        }
        System.out.println("-----------------------");
    }
    
}

輸出結果以下:

field:public int Person.age
-----------------------
field:public java.lang.String Student.desc
field:public int Person.age
field:public java.lang.String Person.name
-----------------------
field2:public java.lang.String Student.desc
-----------------------
field:public java.lang.String Student.desc
field:private int Student.score
-----------------------

上述方法須要注意的是,若是咱們不指望獲取其父類的字段,則需使用Class類的getDeclaredField()/getDeclaredFields()方法來獲取字段便可,假若須要連帶獲取到父類的字段,那麼請使用Class類的getField()/getFields(),可是也只能獲取到public修飾的的字段,沒法獲取父類的私有字段。下面將經過Field類自己的方法對指定類屬性賦值,代碼演示以下:

        Student st = (Student)clazz.newInstance();
        //獲取父類public字段並賦值
        Field ageField = clazz.getField("age");
        ageField.set(st, 18);
        Field nameField = clazz.getField("name");
        nameField.set(st, "王傑文");
        
        //只獲取當前類的字段,不獲取父類的字段
        Field descField = clazz.getDeclaredField("desc");
        descField.set(st, "I'm a Student!");
        Field scoreField = clazz.getDeclaredField("score");
        //設置爲可訪問,score是private
        scoreField.setAccessible(true);
        scoreField.set(st, 88);
        
        System.out.println(st);

輸出以下:

name:王傑文 age:18 desc:I'm a Student! score:88

其中的set(Object obj, Object value)方法是Field類自己的方法,用於設置字段的值,相反get(Object obj)則是獲取字段的值,固然關於Field類還有其餘經常使用的方法以下:

方法返回值 方法名稱 方法說明
void set(Object obj, Object value) 將指定對象變量上此 Field 對象表示的字段設置爲指定的新值。
Object get(Object obj) 返回指定對象上此 Field 表示的字段的值
Class<?> getType() 返回一個 Class 對象,它標識了此Field 對象所表示字段的聲明類型。
boolean isEnumConstant() 若是此字段表示枚舉類型的元素則返回 true;不然返回 false
String toGenericString() 返回一個描述此 Field(包括其通常類型)的字符串
String getName() 返回此 Field 對象表示的字段的名稱
Class<?> getDeclaringClass() 返回表示類或接口的 Class 對象,該類或接口聲明由此 Field 對象表示的字段
void setAccessible(boolean flag) 將此對象的 accessible 標誌設置爲指示的布爾值,即設置其可訪問性

上述方法多是較爲經常使用的,事實上在設置值的方法上,Field類還提供了專門針對基本數據類型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法,這裏就不所有列出了,須要時查API文檔便可。須要特別注意的是被final關鍵字修飾的Field字段是安全的,在運行時能夠接收任何修改,但最終其實際值是不會發生改變的。

注意:使用反射在final字段上的修改是安全的,運行時系統會在不拋異常的狀況下接受任何修改嘗試,可是實際上不會發生任何修改。

三、Method類及其用法

Method 提供關於類或接口上單獨某個方法(以及如何訪問該方法)的信息,所反映的方法多是類方法或實例方法(包括抽象方法)。下面是Class類獲取Method對象相關的方法:

方法返回值 方法名稱 方法說明
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一個指定參數的Method對象,該對象反映此 Class 對象所表示的類或接口的方法,該方法能夠是公共、保護、默認(包)訪問或者私有方法,但不能夠是繼承的方法
Method[] getDeclaredMethod() 返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明的全部方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法
Method getMethod(String name, Class<?>... parameterTypes) 返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的public方法,包括繼承的方法
Method[] getMethods() 返回一個包含某些 Method 對象的數組,這些對象反映此 Class 對象所表示的類或接口(包括那些由該類或接口聲明的以及從超類和超接口繼承的那些的類或接口)的全部public方法

一樣經過案例演示上述方法:

import java.lang.reflect.*;

class Shape{
    public void draw() {
        System.out.println("draw");
    }
    
    public void draw(int count,String name) {
        System.out.println("draw" + name + ",count=" + count);        
    }
}


class Circle extends Shape{
    private void drawCircle() {
        System.out.println("drawCircle");
    }
    
    public int getAllCount() {
        return 100;
    }
    
}

public class ReflectMethod {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException {
        Class clazz = Class.forName("Circle");
        
        //返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的public方法,包括繼承的方法
        Method method = clazz.getMethod("draw", int.class,String.class);
        System.out.println("method:" + method);
        System.out.println("-----------------------------------------");
        
        
        //返回一個包含某些 Method 對象的數組,這些對象反映此 Class 對象所表示的類或接口(包括那些由該類或接口聲明的以及從超類和超接口繼承的那些的類或接口)的全部public方法
        Method[] methods = clazz.getMethods();
        for(Method m:methods) {
            System.out.println("method:" + m);            
        }
        System.out.println("-----------------------------------------");
        
        
        //返回一個指定參數的Method對象,該對象反映此 Class 對象所表示的類或接口的方法,該方法能夠是公共、保護、默認(包)訪問或者私有方法,但不能夠是繼承的方法
        Method method1 = clazz.getDeclaredMethod("drawCircle");
        System.out.println("method:" + method1);
        System.out.println("-----------------------------------------");
        
        //返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明的全部方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法
        Method[] methods1 = clazz.getDeclaredMethods();
        for(Method m:methods1) {
            System.out.println("method:" + m);            
        }
        System.out.println("-----------------------------------------");
        
        
        
    }
}

輸出以下:

method:public void Shape.draw(int,java.lang.String)
-----------------------------------------
method:public int Circle.getAllCount()
method:public void Shape.draw()
method:public void Shape.draw(int,java.lang.String)
method:public final void java.lang.Object.wait() throws java.lang.InterruptedException
method:public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method:public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method:public boolean java.lang.Object.equals(java.lang.Object)
method:public java.lang.String java.lang.Object.toString()
method:public native int java.lang.Object.hashCode()
method:public final native java.lang.Class java.lang.Object.getClass()
method:public final native void java.lang.Object.notify()
method:public final native void java.lang.Object.notifyAll()
-----------------------------------------
method:private void Circle.drawCircle()
-----------------------------------------
method:private void Circle.drawCircle()
method:public int Circle.getAllCount()
-----------------------------------------

在經過getMethods()方法獲取Method對象時,會把父類的方法也獲取到,如上的輸出結果,把Object類的方法都打印出來了。而getDeclaredMethod()/getDeclaredMethods()方法都只能獲取當前類的方法。咱們在使用時根據狀況選擇便可。下面將演示經過Method對象調用指定類的方法:

        //建立對象
        Circle circle = (Circle) clazz.newInstance();


        //經過Method對象的invoke(Object obj,Object... args)方法調用
        method.invoke(circle,15,"圈圈");

        //修改私有方法的訪問標識
        method1.setAccessible(true);
        method1.invoke(circle);

        //對有返回值得方法操做
        Method method2 = clazz.getDeclaredMethod("getAllCount");
        Integer count = (Integer) method2.invoke(circle);
        System.out.println("count:" + count);

輸出以下:

draw:圈圈,count=15
drawCircle
count:100

在上述代碼中調用方法,使用了Method類的invoke(Object obj,Object... args)第一個參數表明調用的對象,第二個參數傳遞的調用方法的參數。這樣就完成了類方法的動態調用。

方法返回值 方法名稱 方法說明
Object invoke(Object obj, Object... args) 對帶有指定參數的指定對象調用由此 Method 對象表示的底層方法
Class<?> getReturnType() 返回一個 Class 對象,該對象描述了此 Method 對象所表示的方法的正式返回類型,即方法的返回類型
Type getGenericReturnType() 返回表示由此 Method 對象所表示方法的正式返回類型的 Type 對象,也是方法的返回類型
Class<?>[] getParameterTypes() 按照聲明順序返回 Class 對象的數組,這些對象描述了此 Method 對象所表示的方法的形參類型。即返回方法的參數類型組成的數組
Type[] getGenericParameterTypes() 按照聲明順序返回 Type 對象的數組,這些對象描述了此 Method 對象所表示的方法的形參類型的,也是返回方法的參數類型
String getName() 以 String 形式返回此 Method 對象表示的方法名稱,即返回方法的名稱
boolean isVarArgs() 判斷方法是否帶可變參數,若是將此方法聲明爲帶有可變數量的參數,則返回 true;不然,返回 false
String toGenericString() 返回描述此 Method 的字符串,包括類型參數

getReturnType()方法/getGenericReturnType()方法都是獲取Method對象表示的方法的返回類型,只不過前者返回的Class類型後者返回的Type(前面已分析過),Type就是一個接口而已,在Java8中新增一個默認的方法實現,返回的就是參數類型信息。

而getParameterTypes()/getGenericParameterTypes()也是一樣的道理,都是獲取Method對象所表示的方法的參數類型,其餘方法與前面的Field和Constructor是相似的。

四、反射包中的Array類

在Java的java.lang.reflect包中存在着一個能夠操做數組的類,Array,它提供了建立和訪問Java 數組的方法。Array容許在執行 get()或 set()操做進行取值和賦值。在Class類中與數組關聯的方法是:

方法返回值 方法名稱 方法說明
Class<?> getComponentType() 返回表示數組元素類型的Class對象,即數組的類型
boolean isArray() 斷定此 Class 對象是否表示一個數組類

java.lang.reflect.Array中的經常使用靜態方法以下:

方法返回值 方法名稱 方法說明
static Object set(Object array, int index) 返回指定數組對象中索引組件的值
static int getLength(Object array) 以 int 形式返回指定數組對象的長度
static object newInstance(Class<?> componentType, int... dimensions) 建立一個具備指定類型和維度的新數組
static Object newInstance(Class<?> componentType, int length) 建立一個具備指定的組件類型和長度的新數組
static void set(Object array, int index, Object value) 將指定數組對象中索引組件的值設置爲指定的新值

下面經過一個簡單例子來演示這些方法:

import java.lang.reflect.*;

public class ReflectArray {

    public static void main(String[] args) throws ClassNotFoundException {
        
        int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        
        //獲取數組類型的Class對象 即int.class
        Class<?> clazz = array.getClass().getComponentType();
        
        //建立一個具備指定的組件類型和長度的新數組。
        //第一個參數:指定了數組中的每一個元素應該是什麼類型,第二個參數:數組的長度
        Object newArr = Array.newInstance(clazz, 15);
        
        //獲取原數組的長度
        int co = Array.getLength(array);
        
        //賦值原數組到新數組
        System.arraycopy(array, 0, newArr, 0, co);
        for (int i:(int[]) newArr) {
            System.out.print(i+",");
        }

        //建立了一個長度爲10 的字符串數組,
        //接着把索引位置爲6 的元素設爲"hello world!",而後再讀取索引位置爲6 的元素的值
        Class clazz2 = Class.forName("java.lang.String");

        //建立一個長度爲10的字符串數組,在Java中數組也能夠做爲Object對象
        Object array2 = Array.newInstance(clazz2, 10);

        //把字符串數組對象的索引位置爲6的元素設置爲"hello"
        Array.set(array2, 6, "hello world!");

        //得到字符串數組對象的索引位置爲5的元素的值
        String str = (String)Array.get(array2, 6);
        System.out.println();
        System.out.println(str);//hello
    }
}

輸出結果:

1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,
hello world!

經過上述代碼演示,確實能夠利用Array類和反射相結合建立數組,也能夠在運行時獲取和設置數組中元素的值,其實除了上的set()/get()外Array還專門爲8種基本數據類型提供特有的方法,如setInt()/getInt()、setBoolean()/getBoolean(),其餘依次類推,須要使用是能夠查看API文檔便可。除了上述修改數組長度或者建立數組或獲取值或設置值外,能夠利用泛型建立泛型數組以下:

import java.lang.reflect.*;

public class ReflectArray {

    /**
      * 接收一個泛型數組,而後建立一個長度與接收的數組長度同樣的泛型數組,
      * 並把接收的數組的元素複製到新建立的數組中,
      * 最後找出新數組中的最小元素,並打印出來
      * @param a
      * @param <T>
      */
    public static <T extends Comparable<T>> void  FindMinValue(T[] a) {
        //經過反射機制建立相同類型的數組
        T[] b = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length);
        
        for(int i=0;i<a.length;i++) {
            b[i] = a[i];
        }
        
        T min = null;        
        boolean flag = true;
        for(int i=0;i<b.length;i++) {
            if(flag) {
                min = b[i];
                flag = false;
            }
            
            if(b[i].compareTo(min) < 0) {
                min = b[i];
            }
        }
        
        System.out.println(min);
        
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        
        int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    
        
        //獲取數組類型的Class對象 即int.class
        Class<?> clazz = array.getClass().getComponentType();
        
        //建立一個具備指定的組件類型和長度的新數組。
        //第一個參數:指定了數組中的每一個元素應該是什麼類型,第二個參數:數組的長度
        Object newArr = Array.newInstance(clazz, 15);
        
        //獲取原數組的長度
        int co = Array.getLength(array);
        
        //賦值原數組到新數組
        System.arraycopy(array, 0, newArr, 0, co);
        for (int i:(int[]) newArr) {
            System.out.print(i+",");
        }

        //建立了一個長度爲10 的字符串數組,
        //接着把索引位置爲6 的元素設爲"hello world!",而後再讀取索引位置爲6 的元素的值
        Class clazz2 = Class.forName("java.lang.String");

        //建立一個長度爲10的字符串數組,在Java中數組也能夠做爲Object對象
        Object array2 = Array.newInstance(clazz2, 10);

        //把字符串數組對象的索引位置爲6的元素設置爲"hello"
        Array.set(array2, 6, "hello world!");

        //得到字符串數組對象的索引位置爲5的元素的值
        String str = (String)Array.get(array2, 6);
        System.out.println();
        System.out.println(str);//hello
        
        String[] strs = {"za","cb","ca","d","e","f"};
        FindMinValue(strs);
    }
}

輸出以下:

1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,
hello world!
ca

畢竟咱們沒法直接建立泛型數組,有了Array建立泛型數組的問題也就迎刃而解了。

//無效語句,編譯不通
T[] b = new T[a.length];

到這反射中幾個重要而且經常使用的類咱們都基本介紹完了,但更重要是,咱們應該認識到反射機制並無什麼神奇之處。當經過反射與一個未知類型的對象打交道時,JVM只會簡單地檢查這個對象,判斷該對象屬於哪一種類型,同時也應該知道,在使用反射機制建立對象前,必須確保已加載了這個類的Class對象,固然這點徹底沒必要由咱們操做,畢竟只能JVM加載,但必須確保該類的」.class」文件已存在而且JVM可以正確找到。關於Class類的方法在前面咱們只是分析了主要的一些方法,其實Class類的API方法挺多的,建議查看一下API文檔,瀏覽一遍,有個印象也是不錯的選擇,這裏僅列出前面沒有介紹過又可能用到的API:

 /** 
  *    修飾符、父類、實現的接口、註解相關 
  */

//獲取修飾符,返回值可經過Modifier類進行解讀
public native int getModifiers();
//獲取父類,若是爲Object,父類爲null
public native Class<? super T> getSuperclass();
//對於類,爲本身聲明實現的全部接口,對於接口,爲直接擴展的接口,不包括經過父類間接繼承來的
public native Class<?>[] getInterfaces();
//本身聲明的註解
public Annotation[] getDeclaredAnnotations();
//全部的註解,包括繼承獲得的
public Annotation[] getAnnotations();
//獲取或檢查指定類型的註解,包括繼承獲得的
public <A extends Annotation> A getAnnotation(Class<A> annotationClass);
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);

/** 
  *   內部類相關
  */
//獲取全部的public的內部類和接口,包括從父類繼承獲得的
public Class<?>[] getClasses();
//獲取本身聲明的全部的內部類和接口
public Class<?>[] getDeclaredClasses();
//若是當前Class爲內部類,獲取聲明該類的最外部的Class對象
public Class<?> getDeclaringClass();
//若是當前Class爲內部類,獲取直接包含該類的類
public Class<?> getEnclosingClass();
//若是當前Class爲本地類或匿名內部類,返回包含它的方法
public Method getEnclosingMethod();

/** 
  *    Class對象類型判斷相關
  */
//是不是數組
public native boolean isArray();  
//是不是基本類型
public native boolean isPrimitive();
//是不是接口
public native boolean isInterface();
//是不是枚舉
public boolean isEnum();
//是不是註解
public boolean isAnnotation();
//是不是匿名內部類
public boolean isAnonymousClass();
//是不是成員類
public boolean isMemberClass();
//是不是本地類
public boolean isLocalClass(); 

四 動態代理

動態代理看起來好像是個什麼高大上的名詞,但其實並無那麼複雜,直接從字面就很容易理解。動態地代理,能夠猜想一下它的含義,在運行時動態地對某些東西代理,代理它作了其餘事情。先不去搞清楚這個動態代理真正的含義,咱們來舉個生動的例子來理解下它到底作了什麼。

一、簡單案例

一個程序員Developer,他會開發code,他調試debug:

interface Developer{
    void code();
    void debug();
}

程序員有不少分類,其中有Java程序員JavaDeveloper,他會開發Java代碼,會調試Java代碼。

class JavaDeveloper implements Developer{
    private String name;
    
    public JavaDeveloper(String name) {
        this.name = name;
    }
    
    @Override
    public void code() {
        // TODO Auto-generated method stub
        System.out.println(this.name + " is coding java");
        
    }

    @Override
    public void debug() {
        // TODO Auto-generated method stub
        System.out.println(this.name + " is debugging java");
    }
    
}
可是呢,有個叫Zack的Java程序員它在開發以前,會祈禱一下,這樣他開發的代碼就不會有bug。Zack的這種「特異功能」是後天練出來的,並無哪一種程序員有這種特性。雖然咱們也能夠定義一個擁有這樣特性的程序員,可是擁有各類亂七八糟特性的程序千千萬。咱們何時才能定義完,而能保證不漏呢?

其實咱們沒有必要去定義他,由於他是後天養成的,咱們應該在這個程序員的成長期去實現這個特性,而不是在他出生以前定義。咱們來看下代碼是怎麼實現的:

public class JavaDynamicProxy {
    
    public static void main(String[] args) {
        JavaDeveloper zack = new JavaDeveloper("Zack");
        zack.code();
        zack.debug();
        
        //建立動態代理
        Developer zackProxy = (Developer)Proxy.newProxyInstance(
                    zack.getClass().getClassLoader(),                  //類加載器
                    zack.getClass().getInterfaces(),                   //但願被代理的接口列表
                    (proxy,method,agrs) -> {
                        if(method.getName().equals("code")) {
                            System.out.println("Zack is praying for the code");
                        }
                        if(method.getName().equals("debug")) {
                            System.out.println("Zack's have no bug! No need to debug!");
                        }
                        //return method.invoke(proxy, agrs);
                        return null;
                    }
                );
        zackProxy.code();
        zackProxy.debug();
    }            
}

若是Zack只是一個普通的Java程序員,那麼他的開發結果是:

Zack is coding java
Zack is debugging java

可是真正的Zack(代理後):

Zack is praying for the code
Zack's have no bug! No need to debug!

二、Proxy.newProxyInstance()

回看下上面是如何使用動態代理的使用。生成一個實例對象zack,而後用Proxy的newInstance方法對這個實例對象代理生成一個動態代理對象zackProxy。

//建立動態代理
        Developer zackProxy = (Developer)Proxy.newProxyInstance(
                    zack.getClass().getClassLoader(),                  //類加載器
                    zack.getClass().getInterfaces(),                   //但願被代理的接口列表
                    (proxy,method,agrs) -> {
                        if(method.getName().equals("code")) {
                            System.out.println("Zack is praying for the code");
                        }
                        if(method.getName().equals("debug")) {
                            System.out.println("Zack's have no bug! No need to debug!");
                        }
                        //return method.invoke(proxy, agrs);
                        return null;
                    }
                );

看下newProxyInstance()的接口定義:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

這三個參數具體的含義來看看註解是怎麼描述的:

  • loader:選用的類加載器。由於代理的是zack,因此通常都會用加載zack的類加載器;
  • interfaces:被代理的類所實現的接口,這個接口能夠是多個;
  • h:綁定代理類的一個方法,或者是InvacationHandler接口的一個實現;

所以能夠修改第三個參數爲InvacationHandler接口的一個實現,代碼以下:

import java.lang.reflect.*;

interface Developer{
    void code();
    void debug();
}

class JavaDeveloper implements Developer{
    private String name;
    
    public JavaDeveloper(String name) {
        this.name = name;
    }
    
    @Override
    public void code() {
        // TODO Auto-generated method stub
        System.out.println(this.name + " is coding java");
        
    }

    @Override
    public void debug() {
        // TODO Auto-generated method stub
        System.out.println(this.name + " is debugging java");
    }
    
}

//調用處理器
class DynamicProxyHandlerDeveloper implements InvocationHandler{
    private Object proxied;
    
  //傳入被代理對象
    public DynamicProxyHandlerDeveloper(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO Auto-generated method stub
        if(method.getName().equals("code")) {
            System.out.println("Zack is praying for the code");
        }
        if(method.getName().equals("debug")) {
            System.out.println("Zack's have no bug! No need to debug!");
        }
        //return method.invoke(proxied, agrs);    //被代理對象zack,這裏能夠將請求轉發給本來方法;
        //return method.invoke(proxy, agrs);      //代理對象zackProxy,會陷入死循環
        return null;        
    }
    
}

public class JavaDynamicProxy {
    
    public static void main(String[] args) {
        JavaDeveloper zack = new JavaDeveloper("Zack");
        zack.code();
        zack.debug();
        
        //建立動態代理
        Developer zackProxy = (Developer)Proxy.newProxyInstance(
                zack.getClass().getClassLoader(),                  //類加載器
                zack.getClass().getInterfaces(),                   //但願被代理的接口列表
                new DynamicProxyHandlerDeveloper(zack)
            );
        zackProxy.code();
        zackProxy.debug();
    }            
}

loader和interfaces基本就是決定了這個類究竟是個怎麼樣的類。而h是InvocationHandler,決定了這個代理類究竟是多了什麼功能。因此動態代理的內容重點就是這個InvocationHandler。

三、InvocationHandler

InvocationHandler做用就是,當被代理對象zack的本來方法被調用的時候,會綁定執行一個方法,這個方法就是InvocationHandler裏面定義的內容,同時會替代本來方法的結果返回:

InvocationHandler接收三個參數

  • proxy:動態代理類對象(zackProxy);
  • method:被代理對象(zack)被調用方法;
  • args:調用時的參數;

在上面的例子裏:

(proxy,method,agrs) -> {
                        if(method.getName().equals("code")) {
                            System.out.println("Zack is praying for the code");
                        }
                        if(method.getName().equals("debug")) {
                            System.out.println("Zack's have no bug! No need to debug!");
                        }
                        //return method.invoke(proxy, agrs);
                        return null;
                    }

若是最後的return語句改爲:

return method.invoke(proxy, agrs);
invoke的對象不是zack,而是proxy,根據上面的說明猜猜會發生什麼?
是的,會不停地循環調用。由於proxy是代理類的對象,當該對象方法被調用的時候,會觸發InvocationHandler,而InvocationHandler裏面又調用一次proxy裏面的對象,因此會不停地循環調用。而且,proxy對應的方法是沒有實現的。因此是會循環的不停報錯。

四、動態代理的使用場景

動態代理的好處咱們從例子就能看出來,它比較靈活,能夠在運行的時候才切入改變類的方法,而不須要預先定義它。

動態代理通常咱們比較少去手寫,但咱們用得其實很是多。在Spring項目中用的註解,例如依賴注入的@Bean、@Autowired,事務註解@Transactional等都有用到,換言之就是Srping的AOP(切面編程)。

這種場景的使用是動態代理最佳的落地點,能夠很是靈活地在某個類,某個方法,某個代碼點上切入咱們想要的內容,就是動態代理其中的內容。

參考文章

[1] 深刻理解Java類型信息(Class對象)與反射機制(轉載)

[2]Java編程思想

[3] 譯】1. Java反射——引言

[4] 你真的徹底瞭解Java動態代理嗎?看這篇就夠了(轉載)

相關文章
相關標籤/搜索