Java類型信息(Class對象)與反射機制


一、RTTI的概念以及Class對象作用

   RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是C++中的概念,至於Java中出現RTTI的說法則是源於《Thinking in java》一書,其作用是在運行時識別一個對象的類型和類的信息。

  這里分為兩種:

  1、傳統的“RTTI”,它假定我們在編譯期已經知道了所有類型(在沒有反射機制創建和使用類對象時,一般都是編譯期已確定其類型,如new對象時該類必須已定義好);

  2、反射機制,它允許我們在運行時發現和使用類型的信息。

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

 1 public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
 2     private static final int ANNOTATION= 0x00002000;
 3     private static final int ENUM      = 0x00004000;
 4     private static final int SYNTHETIC = 0x00001000;
 5  
 6     private static native void registerNatives();
 7     static {
 8         registerNatives();
 9     }
10  
11     /*
12      * Private constructor. Only the Java Virtual Machine creates Class objects.(私有構造,只能由JVM創建該類)
13      * This constructor is not used and prevents the default constructor being
14      * generated.
15      */
16     private Class(ClassLoader loader) {
17         // Initialize final field for classLoader.  The initialization value of non-null
18         // prevents future JIT optimizations from assuming this final field is null.
19         classLoader = loader;
20     }

  Class類被創建后的對象就是Class對象,注意,Class對象表示的是自己手動編寫類的類型信息,比如創建一個Shapes類,那么,JVM就會創建一個Shapes對應Class類的Class對象,該Class對象保存了Shapes類相關的類型信息。

  實際上在java中每個類都有一個Class對象,每當我們編寫並且編譯一個新創建的類就或產生一個對應Class對象並且這個Class對象會被保存在同名.class文件里(編譯后的字節碼文件保存的就是Class對象),那么為什么需要這樣一個Class對象呢?

  是這樣的,當我們new一個新對象或者引用靜態成員變量時,Java虛擬機中的類加載器子系統會將對應Class對象加載到JVM中,然后JVM再根據這個類型信息相關的Class對象創建我們需要實例對象或者提供靜態變量的引用值。

  需要特別注意的是,手動編寫的每個Class類,無論創建多少個實例對象,在JVM中都只有一個Class對象,即在內存中每個類有且只有一個相對應的Class對象,挺拗口,通過下圖理解內存中簡易現象圖:

  到這里我們也就可以得出如下幾點信息:

    1、Class類也是類的一種,與class關鍵字是不一樣的。

    2、手動編寫的類被編譯后會產生一個Class對象,其表示的是創建的類的類型信息,而且對象保存在同名.class的文件中(字節碼文件),比如創建一個Shapes類,編譯shapes類后就會創建其包含Shapes類相關類型信息的Class對象,並保存在Shapes.class字節碼文件中。

    3、每個通過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,無論創建多少個實例對象,其依據的都是用一個Class對象。

    4、Class類只存私有構造函數,因此對應Class對象只能有JVM創建和加載

    5、Class類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要。

二、Class對象的加載及其獲取方式

1、Class對象的加載

  所有的類都是在對其第一次使用時動態加載到JVM中,當程序創建第一個對類的靜態成員引用時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),包含new(構造函數也是類的靜態方法)

  java程序在它們開始運行之前並非完全加載到內存的,類加載器首先檢查對象是否已被加載(類的實例對象創建時依據class對象中類型信息完成的),如果沒有,默認的類加載器就會先根據類名查找.class文件,在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞且不包含不良Java代碼(這是java的安全機制檢測),完全沒有問題后會被動態加載到內存中,此時相當於Class對象也就被載入了內存了,同時也就可以被用來創建這個類的所有實例對象。

 1 class Candy {
 2   static {   System.out.println("Loading Candy"); }
 3 }
 4  
 5 class Gum {
 6   static {   System.out.println("Loading Gum"); }
 7 }
 8  
 9 class Cookie {
10   static {   System.out.println("Loading Cookie"); }
11 }
12  
13 public class SweetShop {
14   public static void print(Object obj) {
15     System.out.println(obj);
16   }
17   public static void main(String[] args) {  
18     print("inside main");
19     new Candy();
20     print("After creating Candy");
21     try {
22       Class.forName("com.wangjun.Gum");
23     } catch(ClassNotFoundException e) {
24       print("Couldn't find Gum");
25     }
26     print("After Class.forName(\"com.wangjun.Gum\")");
27     new Cookie();
28     print("After creating Cookie");
29   }
30 }

  在上述代碼中,每個類Candy、Gun、Cookie都存在一個static語句,這個語句會在類第一次被加載時執行,這個語句的作用就是告訴我們該類在什么時候被加載,執行結果是:

 1 inside main
 2  
 3 Loading Candy
 4  
 5 After creating Candy
 6  
 7 Loading Gum
 8  
 9 After Class.forName("com.wangjun.Gum")
10  
11 Loading Cookie
12  
13 After creating Cookie

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

Class.forName("com.wangjun.Gum");

  其中forName方法是Class類的一個static成員方法,記住所有的Class對象都源於這個Class類,因此Class類中定義的方法將適應所有Class對象。這里通過forName方法,我們可以獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會導致Gum類被加載(前提是Gum類從來沒有被加載過)。

2、Class.forName方法

  通過上述案例,我們也就知道Class.forName()方法的調用將會返回一個對應類的Class對象,因此如果我們想要獲取一個類的運行時類型信息並加以使用時,可以調用Class.forName()方法獲取Class對象的引用,這樣做的好處是無需通過持有該類的實例對象引用而去獲取Class對象,如下的第二種方式是通過一個實例對象獲取一個類的class對象,其中的getClass()是從頂級類Object繼承而來的,它將犯規表示該對象的實際類型的Class對象引用。

 1 public static void main(String[] args) {
 2  
 3     try{
 4       //通過Class.forName獲取Gum類的Class對象
 5       Class clazz=Class.forName("com.wangjun.Gum");
 6       System.out.println("forName=clazz:"+clazz.getName());
 7     }catch (ClassNotFoundException e){
 8       e.printStackTrace();
 9     }
10  
11     //通過實例對象獲取Gum的Class對象
12     Gum gum = new Gum();
13     Class clazz2=gum.getClass();
14     System.out.println("new=clazz2:"+clazz2.getName());
15  
16   }

  注意調用forName方法時需要補貨一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字符串對應的類是否是存在的,只能在程序運行時進行檢查,如果不存在就會拋出ClassNotFoundException異常。

3、Class字面常量

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

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

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

1 boolean.class = Boolean.TYPE;
2 char.class = Character.TYPE;
3 byte.class = Byte.TYPE;
4 short.class = Short.TYPE;
5 int.class = Integer.TYPE;
6 long.class = Long.TYPE;
7 float.class = Float.TYPE;
8 double.class = Double.TYPE;
9 void.class = Void.TYPE;

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

    1、加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節碼文件,並利用字節碼文件創建一個Class對象

    2、鏈接:驗證字節碼的安全性和完整性,准備階段正式為靜態域分配存儲空間,注意此時只是分配靜態成員變量的存儲空間,不包含實例成員變量,如果必要的話,解析這個類創建的對其他類的所有引用。

    3、初始化:類加載最后階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量。

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

 1 import java.util.*;
 2  
 3 class Initable {
 4   //編譯期靜態常量
 5   static final int staticFinal = 47;
 6   //非編期靜態常量
 7   static final int staticFinal2 =
 8     ClassInitialization.rand.nextInt(1000);
 9   static {
10     System.out.println("Initializing Initable");
11   }
12 }
13  
14 class Initable2 {
15   //靜態成員變量
16   static int staticNonFinal = 147;
17   static {
18     System.out.println("Initializing Initable2");
19   }
20 }
21  
22 class Initable3 {
23   //靜態成員變量
24   static int staticNonFinal = 74;
25   static {
26     System.out.println("Initializing Initable3");
27   }
28 }
29  
30 public class ClassInitialization {
31   public static Random rand = new Random(47);
32   public static void main(String[] args) throws Exception {
33     //字面常量獲取方式獲取Class對象
34     Class initable = Initable.class;
35     System.out.println("After creating Initable ref");
36     //不觸發類初始化
37     System.out.println(Initable.staticFinal);
38     //會觸發類初始化
39     System.out.println(Initable.staticFinal2);
40     //會觸發類初始化
41     System.out.println(Initable2.staticNonFinal);
42     //forName方法獲取Class對象
43     Class initable3 = Class.forName("Initable3");
44     System.out.println("After creating Initable3 ref");
45     System.out.println(Initable3.staticNonFinal);
46   }
47 }
執行結果:

1 After creating Initable ref
2 47
3 Initializing Initable
4 258
5 Initializing Initable2
6 147
7 Initializing Initable3
8 After creating Initable3 ref
9 74

 

從輸出結果來看,可以發現
  1.通過字面常量獲取方式獲取Initable類的Class對象並沒有觸發Initable類的初始化,這點也驗證了前面的分析;

  2.同時發現調用Initable.static final變量時也沒有觸發初始化,這是因為static final屬於編譯期靜態常量,在編譯階段通過常量傳播優化的方式將Initable類的常量static final存儲到了一個稱為NotInitialization類的常量池中,在以后對Initable類常量static final的引用實際都轉化為對NotInitialization類對自身常量池的引用,所以在編譯期后,對編譯期常量的引用都將在NotInitialization類的常量池獲取,這也就是引用編譯期靜態常量不會觸發Initable類初始化的重要原因。但在之后調用了Initable.staticFinal2變量后就觸發了Initable類的初始化,注意staticFinal2雖然被static和final修飾,但其值在編譯期並不能確定,因此staticFinal2並不是編譯期常量,使用該變量必須先初始化Initable類。

  3.Initable2和Initable3類中都是靜態成員變量並非編譯期常量,引用都會觸發初始化。至於forName方法獲取Class對象,肯定會觸發初始化,這點在前面已分析過。

到這幾種獲取Class對象的方式也都分析完,ok~,到此這里可以得出小結論:

獲取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的問題:

1 //編譯通過!
2 Class<? extends Number> clazz = Integer.class;
3 //賦予其他類型
4 clazz = double.class;
5 clazz = Number.class;

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

三、關於類型轉換的問題

  在許多需要強制類型轉換的場景,我們更多的做法是直接強制轉換類型:

 1 public class ClassCast {
 2  
 3  public void cast(){
 4  
 5      Animal animal= new Dog();
 6      //強制轉換
 7      Dog dog = (Dog) animal;
 8  }
 9 }
10  
11 interface Animal{ }
12  
13 class Dog implements  Animal{ }

  

  之所可以強制轉換,這得歸功於RTTI,要知道在Java中,所有類型轉換都是在運行時進行正確性檢查的,利用RTTI進行判斷類型是否正確從而確保強制轉換的完成,如果類型轉換失敗,將會拋出類型轉換異常。除了強制轉換外,在Java SE5中新增一種使用Class對象進行類型轉換的方式,如下:

1 Animal animal= new Dog();
2 //這兩句等同於Dog dog = (Dog) animal;
3 Class<Dog> dogType = Dog.class;
4 Dog dog = dogType.cast(animal)

  利用Class對象的cast方法,其參數接收一個參數對象並將其轉換為Class引用的類型。這種方式似乎比之前的強制轉換更麻煩些,確實如此,而且當類型不能正確轉換時,仍然會拋出ClassCastException異常。源碼如下:

1 public T cast(Object obj) {
2     if (obj != null && !isInstance(obj))
3          throw new ClassCastException(cannotCastMsg(obj));
4      return (T) obj;
5   }

四、instanceof 關鍵字與isInstance方法

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

1 public void cast2(Object obj){
2     if(obj instanceof Animal){
3           Animal animal= (Animal) obj;
4       }
5 }

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

 1 public void cast2(Object obj){
 2         //instanceof關鍵字
 3         if(obj instanceof Animal){
 4             Animal animal= (Animal) obj;
 5         }
 6  
 7         //isInstance方法
 8         if(Animal.class.isInstance(obj)){
 9             Animal animal= (Animal) obj;
10         }
11   }

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

1 //判斷這個對象是不是這種類型
2 obj.instanceof(class)

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

1 //判斷這個對象能不能被轉化為這個類
2 class.inInstance(obj)

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

 1 class A {}
 2  
 3 class B extends A {}
 4  
 5 public class C {
 6   static void test(Object x) {
 7     print("Testing x of type " + x.getClass());
 8     print("x instanceof A " + (x instanceof A));
 9     print("x instanceof B "+ (x instanceof B));
10     print("A.isInstance(x) "+ A.class.isInstance(x));
11     print("B.isInstance(x) " +
12       B.class.isInstance(x));
13     print("x.getClass() == A.class " +
14       (x.getClass() == A.class));
15     print("x.getClass() == B.class " +
16       (x.getClass() == B.class));
17     print("x.getClass().equals(A.class)) "+
18       (x.getClass().equals(A.class)));
19     print("x.getClass().equals(B.class)) " +
20       (x.getClass().equals(B.class)));
21   }
22   public static void main(String[] args) {
23     test(new A());
24     test(new B());
25   } 
26 }

執行結果:

 1 Testing x of type class com.wangjun.A
 2 x instanceof A true
 3 x instanceof B false //父類不一定是子類的某個類型
 4 A.isInstance(x) true
 5 B.isInstance(x) false
 6 x.getClass() == A.class true
 7 x.getClass() == B.class false
 8 x.getClass().equals(A.class)) true
 9 x.getClass().equals(B.class)) false
10 ---------------------------------------------
11 Testing x of type class com.wangjun.B
12 x instanceof A true
13 x instanceof B true
14 A.isInstance(x) true
15 B.isInstance(x) true
16 x.getClass() == A.class false
17 x.getClass() == B.class true
18 x.getClass().equals(A.class)) false
19 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),下面將對這幾個重要類進行分別說明。

1、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對象的使用:

 1 package reflect;
 2  
 3 import java.io.Serializable;
 4 import java.lang.reflect.Constructor;
 5  
 6 /**
 7  * Created by wangjun on 2017/5/1.
 8  * Blog : http://blog.csdn.net/javawangjun [原文地址,請尊重原創]
 9  */
10 public class ReflectDemo implements Serializable{
11     public static void main(String[] args) throws Exception {
12  
13         Class<?> clazz = null;
14  
15         //獲取Class對象的引用
16         clazz = Class.forName("reflect.User");
17  
18         //第一種方法,實例化默認構造方法,User必須無參構造函數,否則將拋異常
19         User user = (User) clazz.newInstance();
20         user.setAge(20);
21         user.setName("Rollen");
22         System.out.println(user);
23  
24         System.out.println("--------------------------------------------");
25  
26         //獲取帶String參數的public構造函數
27         Constructor cs1 =clazz.getConstructor(String.class);
28         //創建User
29         User user1= (User) cs1.newInstance("xiaolong");
30         user1.setAge(22);
31         System.out.println("user1:"+user1.toString());
32  
33         System.out.println("--------------------------------------------");
34  
35         //取得指定帶int和String參數構造函數,該方法是私有構造private
36         Constructor cs2=clazz.getDeclaredConstructor(int.class,String.class);
37         //由於是private必須設置可訪問
38         cs2.setAccessible(true);
39         //創建user對象
40         User user2= (User) cs2.newInstance(25,"lidakang");
41         System.out.println("user2:"+user2.toString());
42  
43         System.out.println("--------------------------------------------");
44  
45         //獲取所有構造包含private
46         Constructor<?> cons[] = clazz.getDeclaredConstructors();
47         // 查看每個構造方法需要的參數
48         for (int i = 0; i < cons.length; i++) {
49             //獲取構造函數參數類型
50             Class<?> clazzs[] = cons[i].getParameterTypes();
51             System.out.println("構造函數["+i+"]:"+cons[i].toString() );
52             System.out.print("參數類型["+i+"]:(");
53             for (int j = 0; j < clazzs.length; j++) {
54                 if (j == clazzs.length - 1)
55                     System.out.print(clazzs[j].getName());
56                 else
57                     System.out.print(clazzs[j].getName() + ",");
58             }
59             System.out.println(")");
60         }
61     }
62 }
63  
64  
65 class User {
66     private int age;
67     private String name;
68     public User() {
69         super();
70     }
71     public User(String name) {
72         super();
73         this.name = name;
74     }
75  
76     /**
77      * 私有構造
78      * @param age
79      * @param name
80      */
81     private User(int age, String name) {
82         super();
83         this.age = age;
84         this.name = name;
85     }
86  
87   //..........省略set 和 get方法
88 }

運行結果:

 1 User [age=20, name=Rollen]
 2 --------------------------------------------
 3 user1:User [age=22, name=xiaolong]
 4 --------------------------------------------
 5 user2:User [age=25, name=lidakang]
 6 --------------------------------------------
 7 構造函數[0]:private reflect.User(int,java.lang.String)
 8 參數類型[0]:(int,java.lang.String)
 9 構造函數[1]:public reflect.User(java.lang.String)
10 參數類型[1]:(java.lang.String)
11 構造函數[2]:public reflect.User()
12 參數類型[2]:()

1.1關於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 的字符串,其中包括類型參數。

代碼演示如下:

 1 Constructor cs3=clazz.getDeclaredConstructor(int.class,String.class);
 2  
 3 System.out.println("-----getDeclaringClass-----");
 4 Class uclazz=cs3.getDeclaringClass();
 5 //Constructor對象表示的構造方法的類
 6 System.out.println("構造方法的類:"+uclazz.getName());
 7  
 8 System.out.println("-----getGenericParameterTypes-----");
 9 //對象表示此 Constructor 對象所表示的方法的形參類型
10 Type[] tps=cs3.getGenericParameterTypes();
11 for (Type tp:tps) {
12     System.out.println("參數名稱tp:"+tp);
13 }
14 System.out.println("-----getParameterTypes-----");
15 //獲取構造函數參數類型
16 Class<?> clazzs[] = cs3.getParameterTypes();
17 for (Class claz:clazzs) {
18     System.out.println("參數名稱:"+claz.getName());
19 }
20 System.out.println("-----getName-----");
21 //以字符串形式返回此構造方法的名稱
22 System.out.println("getName:"+cs3.getName());
23  
24 System.out.println("-----getoGenericString-----");
25 //返回描述此 Constructor 的字符串,其中包括類型參數。
26 System.out.println("getoGenericString():"+cs3.toGenericString());

輸出結果

 1  
 2  -----getDeclaringClass-----
 3  構造方法的類:reflect.User
 4  -----getGenericParameterTypes-----
 5  參數名稱tp:int
 6  參數名稱tp:class java.lang.String
 7  -----getParameterTypes-----
 8  參數名稱:int
 9  參數名稱:java.lang.String
10  -----getName-----
11  getName:reflect.User
12  -----getoGenericString-----
13  getoGenericString():private reflect.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類,我們可以在運行時動態創建任意對象,從而突破必須在編譯期知道確切類型的障礙。

2.Field類及其用法

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

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

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

 1 public class ReflectField {
 2  
 3     public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
 4         Class<?> clazz = Class.forName("reflect.Student");
 5         //獲取指定字段名稱的Field類,注意字段修飾符必須為public而且存在該字段,
 6         // 否則拋NoSuchFieldException
 7         Field field = clazz.getField("age");
 8         System.out.println("field:"+field);
 9  
10         //獲取所有修飾符為public的字段,包含父類字段,注意修飾符為public才會獲取
11         Field fields[] = clazz.getFields();
12         for (Field f:fields) {
13             System.out.println("f:"+f.getDeclaringClass());
14         }
15  
16         System.out.println("================getDeclaredFields====================");
17         //獲取當前類所字段(包含private字段),注意不包含父類的字段
18         Field fields2[] = clazz.getDeclaredFields();
19         for (Field f:fields2) {
20             System.out.println("f2:"+f.getDeclaringClass());
21         }
22         //獲取指定字段名稱的Field類,可以是任意修飾符的自動,注意不包含父類的字段
23         Field field2 = clazz.getDeclaredField("desc");
24         System.out.println("field2:"+field2);
25     }
26  
27 }
28  
29 class Person{
30     public int age;
31     public String name;
32     //省略set和get方法
33 }
34  
35 class Student extends Person{
36     public String desc;
37     private int score;
38     //省略set和get方法
39 }

輸出結果:

1 field:public int reflect.Person.age
2 f:public java.lang.String reflect.Student.desc
3 f:public int reflect.Person.age
4 f:public java.lang.String reflect.Person.name
5  
6 ================getDeclaredFields====================
7 f2:public java.lang.String reflect.Student.desc
8 f2:private int reflect.Student.score
9 field2:public java.lang.String reflect.Student.desc

  

  上述方法需要注意的是,如果我們不期望獲取其父類的字段,則需使用Class類的getDeclaredField/getDeclaredFields方法來獲取字段即可,倘若需要連帶獲取到父類的字段,那么請使用Class類的getField/getFields,但是也只能獲取到public修飾的的字段,無法獲取父類的私有字段。下面將通過Field類本身的方法對指定類屬性賦值,代碼演示如下:

 1 //獲取Class對象引用
 2 Class<?> clazz = Class.forName("reflect.Student");
 3  
 4 Student st= (Student) clazz.newInstance();
 5 //獲取父類public字段並賦值
 6 Field ageField = clazz.getField("age");
 7 ageField.set(st,18);
 8 Field nameField = clazz.getField("name");
 9 nameField.set(st,"Lily");
10  
11 //只獲取當前類的字段,不獲取父類的字段
12 Field descField = clazz.getDeclaredField("desc");
13 descField.set(st,"I am student");
14 Field scoreField = clazz.getDeclaredField("score");
15 //設置可訪問,score是private的
16 scoreField.setAccessible(true);
17 scoreField.set(st,88);
18 System.out.println(st.toString());
19  
20 //輸出結果:Student{age=18, name='Lily ,desc='I am student', score=88} 
21  
22 //獲取字段值
23 System.out.println(scoreField.get(st));
24 // 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字段是安全的,在運行時可以接收任何修改,但最終其實際值是不會發生改變的。

3.Method類及其用法

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

方法返回值 方法名稱 方法說明
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一個指定參數的Method對象,該對象反映此 Class 對象所表示的類或接口的指定已聲明方法。
Method[] getDeclaredMethod() 返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明的所有方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法。
Method getMethod(String name, Class<?>... parameterTypes) 返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的指定公共成員方法。
Method[]

getMethods() 返回一個包含某些 Method 對象的數組,這些對象反映此 Class 對象所表示的類或接口(包括那些由該類或接口聲明的以及從超類和超接口繼承的那些的類或接口)的公共 member 方法。

同樣通過案例演示上述方法:

 1 public class ReflectMethod  {
 2  
 3  
 4     public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
 5  
 6         Class clazz = Class.forName("reflect.Circle");
 7  
 8         //根據參數獲取public的Method,包含繼承自父類的方法
 9         Method method = clazz.getMethod("draw",int.class,String.class);
10  
11         System.out.println("method:"+method);
12  
13         //獲取所有public的方法:
14         Method[] methods =clazz.getMethods();
15         for (Method m:methods){
16             System.out.println("m::"+m);
17         }
18  
19         System.out.println("=========================================");
20  
21         //獲取當前類的方法包含private,該方法無法獲取繼承自父類的method
22         Method method1 = clazz.getDeclaredMethod("drawCircle");
23         System.out.println("method1::"+method1);
24         //獲取當前類的所有方法包含private,該方法無法獲取繼承自父類的method
25         Method[] methods1=clazz.getDeclaredMethods();
26         for (Method m:methods1){
27             System.out.println("m1::"+m);
28         }
29     }
30  
31 }
32  
33 class Shape {
34     public void draw(){
35         System.out.println("draw");
36     }
37  
38     public void draw(int count , String name){
39         System.out.println("draw "+ name +",count="+count);
40     }
41  
42 }
43 class Circle extends Shape{
44  
45     private void drawCircle(){
46         System.out.println("drawCircle");
47     }
48     public int getAllCount(){
49         return 100;
50     }
51 }

輸出結果:

 1 輸出結果:
 2 method:public void reflect.Shape.draw(int,java.lang.String)
 3 m::public int reflect.Circle.getAllCount()
 4 m::public void reflect.Shape.draw()
 5 m::public void reflect.Shape.draw(int,java.lang.String)
 6 m::public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
 7 m::public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
 8 m::public final void java.lang.Object.wait() throws java.lang.InterruptedException
 9 m::public boolean java.lang.Object.equals(java.lang.Object)
10 m::public java.lang.String java.lang.Object.toString()
11 m::public native int java.lang.Object.hashCode()
12 m::public final native java.lang.Class java.lang.Object.getClass()
13 m::public final native void java.lang.Object.notify()
14 m::public final native void java.lang.Object.notifyAll()
15  
16 =========================================
17 method1::private void reflect.Circle.drawCircle()
18  
19 m1::public int reflect.Circle.getAllCount()
20 m1::private void reflect.Circle.drawCircle()

  

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

 1 Class clazz = Class.forName("reflect.Circle");
 2 //創建對象
 3 Circle circle = (Circle) clazz.newInstance();
 4  
 5 //獲取指定參數的方法對象Method
 6 Method method = clazz.getMethod("draw",int.class,String.class);
 7  
 8 //通過Method對象的invoke(Object obj,Object... args)方法調用
 9 method.invoke(circle,15,"圈圈");
10  
11 //對私有無參方法的操作
12 Method method1 = clazz.getDeclaredMethod("drawCircle");
13 //修改私有方法的訪問標識
14 method1.setAccessible(true);
15 method1.invoke(circle);
16  
17 //對有返回值得方法操作
18 Method method2 =clazz.getDeclaredMethod("getAllCount");
19 Integer count = (Integer) method2.invoke(circle);
20 System.out.println("count:"+count);

輸出結果:

1 draw 圈圈,count=15
2 drawCircle
3 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中新增一個默認的方法實現,返回的就參數類型信息。

1 public interface Type {
2     //1.8新增
3     default String getTypeName() {
4         return toString();
5     }
6 }

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

4、反射包中的Array類

  在Java的java.lang.reflect包中存在着一個可以動態操作數組的類,Array,它提供了動態創建和訪問 Java 數組的方法。Array 允許在執行 get 或 set 操作進行取值和賦值。在Class類中與數組關聯的方法是:

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

  java.lang.reflect.Array中的常用靜態方法如下:

方法返回值 方法名稱 方法說明
static Objec 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) 將指定數組對象中索引組件的值設置為指定的新值。

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

 1 import java.lang.reflect.Array;
 2  
 3 public class ReflectArray {
 4  
 5     public static void main(String[] args) throws ClassNotFoundException {
 6         int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 7         //獲取數組類型的Class 即int.class
 8         Class<?> clazz = array.getClass().getComponentType();
 9         //創建一個具有指定的組件類型和長度的新數組。
10         //第一個參數:數組的類型,第二個參數:數組的長度
11         Object newArr = Array.newInstance(clazz, 15);
12         //獲取原數組的長度
13         int co = Array.getLength(array);
14         //賦值原數組到新數組
15         System.arraycopy(array, 0, newArr, 0, co);
16         for (int i:(int[]) newArr) {
17             System.out.print(i+",");
18         }
19  
20         //創建了一個長度為10 的字符串數組,
21         //接着把索引位置為6 的元素設為"hello world!",然后再讀取索引位置為6 的元素的值
22         Class clazz2 = Class.forName("java.lang.String");
23  
24         //創建一個長度為10的字符串數組,在Java中數組也可以作為Object對象
25         Object array2 = Array.newInstance(clazz2, 10);
26  
27         //把字符串數組對象的索引位置為6的元素設置為"hello"
28         Array.set(array2, 6, "hello world!");
29  
30         //獲得字符串數組對象的索引位置為5的元素的值
31         String str = (String)Array.get(array2, 6);
32         System.out.println();
33         System.out.println(str);//hello
34     }
35    
36 }

輸出結果:

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

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

 1 /**
 2   * 接收一個泛型數組,然后創建一個長度與接收的數組長度一樣的泛型數組,
 3   * 並把接收的數組的元素復制到新創建的數組中,
 4   * 最后找出新數組中的最小元素,並打印出來
 5   * @param a
 6   * @param <T>
 7   */
 8  public  <T extends Comparable<T>> void min(T[] a) {
 9      //通過反射創建相同類型的數組
10      T[] b = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length);
11      for (int i = 0; i < a.length; i++) {
12          b[i] = a[i];
13      }
14      T min = null;
15      boolean flag = true;
16      for (int i = 0; i < b.length; i++) {
17          if (flag) {
18              min = b[i];
19              flag = false;
20          }
21          if (b[i].compareTo(min) < 0) {
22              min = b[i];
23          }
24      }
25      System.out.println(min);
26  }

畢竟我們無法直接創建泛型數組,有了Array的動態創建數組的方式這個問題也就迎刃而解了。

1 //無效語句,編譯不通
2 T[] a = new T[];

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

 1 /** 
 2   *    修飾符、父類、實現的接口、注解相關 
 3   */
 4  
 5 //獲取修飾符,返回值可通過Modifier類進行解讀
 6 public native int getModifiers();
 7 //獲取父類,如果為Object,父類為null
 8 public native Class<? super T> getSuperclass();
 9 //對於類,為自己聲明實現的所有接口,對於接口,為直接擴展的接口,不包括通過父類間接繼承來的
10 public native Class<?>[] getInterfaces();
11 //自己聲明的注解
12 public Annotation[] getDeclaredAnnotations();
13 //所有的注解,包括繼承得到的
14 public Annotation[] getAnnotations();
15 //獲取或檢查指定類型的注解,包括繼承得到的
16 public <A extends Annotation> A getAnnotation(Class<A> annotationClass);
17 public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
18  
19 /** 
20   *   內部類相關
21   */
22 //獲取所有的public的內部類和接口,包括從父類繼承得到的
23 public Class<?>[] getClasses();
24 //獲取自己聲明的所有的內部類和接口
25 public Class<?>[] getDeclaredClasses();
26 //如果當前Class為內部類,獲取聲明該類的最外部的Class對象
27 public Class<?> getDeclaringClass();
28 //如果當前Class為內部類,獲取直接包含該類的類
29 public Class<?> getEnclosingClass();
30 //如果當前Class為本地類或匿名內部類,返回包含它的方法
31 public Method getEnclosingMethod();
32  
33 /** 
34   *    Class對象類型判斷相關
35   */
36 //是否是數組
37 public native boolean isArray();  
38 //是否是基本類型
39 public native boolean isPrimitive();
40 //是否是接口
41 public native boolean isInterface();
42 //是否是枚舉
43 public boolean isEnum();
44 //是否是注解
45 public boolean isAnnotation();
46 //是否是匿名內部類
47 public boolean isAnonymousClass();
48 //是否是成員類
49 public boolean isMemberClass();
50 //是否是本地類
51 public boolean isLocalClass(); 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM