1. 程式人生 > >深入理解Java型別資訊(Class物件)與反射機制

深入理解Java型別資訊(Class物件)與反射機制

關聯文章:

本篇主要是深入對Java中的Class物件進行分析,這對後續深入理解反射技術非常重要,主要內容如下:

深入理解Class物件

RRTI的概念以及Class物件作用

認識Class物件之前,先來了解一個概念,RTTI(Run-Time Type Identification)執行時型別識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RRTI的說法則是源於《Thinking in Java》一書,其作用是在執行時識別一個物件的型別和類的資訊,這裡分兩種:傳統的”RRTI”,它假定我們在編譯期已知道了所有型別(在沒有反射機制建立和使用類物件時,一般都是編譯期已確定其型別,如new物件時該類必須已定義好),另外一種是反射機制,它允許我們在執行時發現和使用型別的資訊。在Java中用來表示執行時型別資訊的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分原始碼如下:

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; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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物件的載入

前面我們已提到過,Class物件是由JVM載入的,那麼其載入時機是?實際上所有的類都是在對其第一次使用時動態載入到JVM中的,當程式建立第一個對類的靜態成員引用時,就會載入這個被使用的類(實際上載入的就是這個類的位元組碼檔案),注意,使用new操作符建立類的新例項物件也會被當作對類的靜態成員的引用(建構函式也是類的靜態方法),由此看來Java程式在它們開始執行之前並非被完全載入到記憶體的,其各個部分是按需載入,所以在使用該類時,類載入器首先會檢查這個類的Class物件是否已被載入(類的例項物件建立時依據Class物件中型別資訊完成的),如果還沒有載入,預設的類載入器就會先根據類名查詢.class檔案(編譯後Class物件被儲存在同名的.class檔案中),在這個類的位元組碼檔案被載入時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java程式碼(這是java的安全機制檢測),完全沒有問題後就會被動態載入到記憶體中,此時相當於Class物件也就被載入記憶體了(畢竟.class位元組碼檔案儲存的就是Class物件),同時也就可以被用來建立這個類的所有例項物件。下面通過一個簡單例子來說明Class物件被載入的時機問題(例子引用自Thinking in Java):

package com.zejian;

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("com.zejian.Gum");
    } catch(ClassNotFoundException e) {
      print("Couldn't find Gum");
    }
    print("After Class.forName(\"com.zejian.Gum\")");
    new Cookie();
    print("After creating Cookie");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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

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

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

從結果來看,new一個Candy物件和Cookie物件,建構函式將被呼叫,屬於靜態方法的引用,Candy類的Class物件和Cookie的Class物件肯定會被載入,畢竟Candy例項物件的建立依據其Class物件。比較有意思的是

Class.forName("com.zejian.Gum");
  • 1

其中forName方法是Class類的一個static成員方法,記住所有的Class物件都源於這個Class類,因此Class類中定義的方法將適應所有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("com.zejian.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());

  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注意呼叫forName方法時需要捕獲一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字串對應的類是否存在的,只能在程式執行時進行檢查,如果不存在就會丟擲ClassNotFoundException異常。

Class字面常量

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

//字面常量的方式獲取Class物件
Class clazz = Gum.class;
  • 1
  • 2

這種方式相對前面兩種方法更加簡單,更安全。因為它在編譯器就會受到編譯器的檢查同時由於無需呼叫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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

前面提到過,使用字面常量的方式獲取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);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

執行結果:

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

從輸出結果來看,可以發現,通過字面常量獲取方式獲取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物件的方式也都分析完,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物件更具體的型別,即使在執行期間會被擦除,但編譯期足以確保我們使用正確的物件型別。如下:

/**
 * Created by zejian on 2017/4/30.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

從程式碼可以看出,宣告普通的Class物件,在編譯器並不會檢查Class物件的確切型別是否符合要求,如果存在錯誤只有在執行時才得以暴露出來。但是通過泛型宣告指明型別的Class物件,編譯器在編譯期將對帶泛型的類進行額外的型別檢查,確保在編譯期就能保證型別的正確性,實際上Integer.class就是一個Class<Integer>類的物件。面對下述語句,確實可能令人困惑,但該語句確實是無法編譯通過的。

//編譯無法通過
Class<Number> numberClass=Integer.class;
  • 1
  • 2

我們或許會想Integer不就是Number的子類嗎?然而事實並非這般簡單,畢竟Integer的Class物件並非Number的Class物件的子類,前面提到過,所有的Class物件都只來源於Class類,看來事實確實如此。當然我們可以利用萬用字元“?”來解決問題:

Class<?> intClass = int.class;
intClass = double.class;
  • 1
  • 2

這樣的語句並沒有什麼問題,畢竟萬用字元指明所有型別都適用,那麼為什麼不直接使用Class還要使用Class<?>呢?這樣做的好處是告訴編譯器,我們是確實是採用任意型別的泛型,而非忘記使用泛型約束,因此Class<?>總是優於直接使用Class,至少前者在編譯器檢查時不會產生警告資訊。當然我們還可以使用extends關鍵字告訴編譯器接收某個型別的子類,如解決前面Number與Integer的問題:

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

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

關於型別轉換的問題

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

package com.zejian;

/**
 * Created by zejian on 2017/4/30.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class ClassCast {

 public void cast(){

     Animal animal= new Dog();
     //強制轉換
     Dog dog = (Dog) animal;
 }
}

interface Animal{ }

class Dog implements  Animal{ }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

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

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

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

instanceof 關鍵字與isInstance方法

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

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

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

public void cast2(Object obj){
        //instanceof關鍵字
        if(obj instanceof Animal){
            Animal animal= (Animal) obj;
        }

        //isInstance方法
        if(Animal.class.isInstance(obj)){
            Animal animal= (Animal) obj;
        }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

//判斷這個物件是不是這種型別
obj.instanceof(class)
  • 1
  • 2

而isInstance方法則是Class類的Native方法,其中obj是被測試的物件或者變數,如果obj是呼叫這個方法的class或介面的例項,則返回true。如果被檢測的物件是null或者基本型別,那麼返回值是false;一般形式如下:

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

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

class A {}

class B extends A {}

public class C {
  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());
  } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

執行結果:

Testing x of type class com.zejian.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 com.zejian.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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

到此關於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物件的使用:

package reflect;

import java.io.Serializable;
import java.lang.reflect.Constructor;

/**
 * Created by zejian on 2017/5/1.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class ReflectDemo implements Serializable{
    public static void main(String[] args) throws Exception {

        Class<?> clazz = null;

        //獲取Class物件的引用
        clazz = Class.forName("reflect.User");

        //第一種方法,例項化預設構造方法,User必須無參建構函式,否則將拋異常
        User user = (User) clazz.newInstance();
        user.setAge(20);
        user.setName("Rollen");
        System.out.println(user);

        System.out.println("--------------------------------------------");

        //獲取帶String引數的public建構函式
        Constructor cs1 =clazz.getConstructor(String.class);
        //建立User
        User user1= (User) cs1.newInstance("xiaolong");
        user1.setAge(22);
        System.out.println("user1:"+user1.toString());

        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,"lidakang");
        System.out.println("user2:"+user2.toString());

        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(")");
        }
    }
}


class User {
    private int age;
    private String name;
    public User() {
        super();
    }
    public User(String name) {
        super();
        this.name = name;
    }

    /**
     * 私有構造
     * @param age
     * @param name
     */
    private User(int age, String name) {
        super();
        this.age = age;
        this.name = name;
    }

  //..........省略set 和 get方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

執行結果:

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

關於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 的字串,其中包括型別引數。

程式碼演示如下:

Constructor cs3=clazz.getDeclaredConstructor(int.class,String.class);

System.out.println("-----getDeclaringClass-----");
Class uclazz=cs3.getDeclaringClass();
//Constructor物件表示的構造方法的類
System.out.println("構造方法的類:"+uclazz.getName());

System.out.println("-----getGenericParameterTypes-----");
//物件表示此 Constructor 物件所表示的方法的形參型別
Type[] tps=cs3.getGenericParameterTypes();
for (Type tp:tps) {
    System.out.println("引數名稱tp:"+tp);
}
System.out.println("-----getParameterTypes-----");
//獲取建構函式引數型別
Class<?> clazzs[] = cs3.getParameterTypes();
for (Class claz:clazzs) {
    System.out.println("引數名稱:"+claz.getName());
}
System.out.println("-----getName-----");
//以字串形式返回此構造方法的名稱
System.out.println("getName:"+cs3.getName());

System.out.println("-----getoGenericString-----");
//返回描述此 Constructor 的字串,其中包括型別引數。
System.out.println("getoGenericString():"+cs3.toGenericString());
/**
 輸出結果:
 -----getDeclaringClass-----
 構造方法的類:reflect.User
 -----getGenericParameterTypes-----
 引數名稱tp:int
 引數名稱tp:class java.lang.String
 -----getParameterTypes-----
 引數名稱:int
 引數名稱:java.lang.String
 -----getName-----
 getName:reflect.User
 -----getoGenericString-----
 getoGenericString():private reflect.User(int,java.lang.String)
 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

其中關於Type型別這裡簡單說明一下,Type 是 Java 程式語言中所有型別的公共高階介面。它們包括原始型別、引數化型別、陣列型別、型別變數和基本型別。getGenericParameterTypesgetParameterTypes 都是獲取構成函式的引數型別,前者返回的是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[] getDeclaredField() 獲取Class物件所表示的類或介面的所有(包含private修飾的)欄位,不包括繼承的欄位
Field getField(String name) 獲取指定name名稱、具有public修飾的欄位,包含繼承欄位
Field[] getField() 獲取修飾符為public的欄位,包含繼承欄位

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

/**
 * Created by zejian on 2017/5/1.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class ReflectField {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class<?> clazz = Class.forName("reflect.Student");
        //獲取指定欄位名稱的Field類,注意欄位修飾符必須為public而且存在該欄位,
        // 否則拋NoSuchFieldException
        Field field = clazz.getField("age");
        System.out.println("field:"+field);

        //獲取所有修飾符為public的欄位,包含父類欄位,注意修飾符為public才會獲取
        Field fields[] = clazz.getFields();
        for (Field f:fields) {
            System.out.println("f:"+f.getDeclaringClass());
        }

        System.out.println("================getDeclaredFields====================");
        //獲取當前類所欄位(包含private欄位),注意不包含父類的欄位
        Field fields2[] = clazz.getDeclaredFields();
        for (Field f:fields2) {
            System.out.println("f2:"+f.getDeclaringClass());
        }
        //獲取指定欄位名稱的Field類,可以是任意修飾符的自動,注意不包含父類的欄位
        Field field2 = clazz.getDeclaredField("desc");
        System.out.println("field2:"+field2);
    }
    /**
      輸出結果: 
     field:public int reflect.Person.age
     f:public java.lang.String reflect.Student.desc
     f:public int reflect.Person.age
     f:public java.lang.String reflect.Person.name

     ================getDeclaredFields====================
     f2:public java.lang.String reflect.Student.desc
     f2:private int reflect.Student.score
     field2:public java.lang.String reflect.Student.desc
     */
}

class Person{
    public int age;
    public String name;
    //省略set和get方法
}

class Student extends Person{
    public String desc;
    private int score;
    //省略set和get方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

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

//獲取Class物件引用
Class<?> clazz = Class.forName("reflect.Student");

Student st= (Student) clazz.newInstance();
//獲取父類public欄位並賦值
Field ageField = clazz.getField("age");
ageField.set(st,18);
Field nameField = clazz.getField("name");
nameField.set(st,"Lily");

//只獲取當前類的欄位,不獲取父類的欄位
Field descField = clazz.getDeclaredField("desc");
descField.set(st,"I am student");
Field scoreField = clazz.getDeclaredField(