java基礎:深入理解Class物件與反射機制

Hiway發表於2018-12-16

其他更多java基礎文章:
java基礎學習(目錄)


深入理解Class物件

RRIT及Class物件的概念

RRIT(Run-Time Type Identification)執行時型別識別。在《Thinking in Java》一書第十四章中有提到,其作用是在執行時識別一個物件的型別和類的資訊。主要有兩種方式:一種是“傳統的”RTTI,它假定我們在編譯時已經知道了所有的型別;另一種是“反射”機制,它允許我們在執行時發現和使用類的資訊。

類是程式的一部分,每個類都有一個class物件。換言之,每當編寫並且編譯了一個新類,就會產生一個Class物件(更恰當地說,是被儲存在一個同名的.class檔案中)。所有的類都是在對其第一次使用時,動態載入到JVM中的。例如我們寫了一個Test類,編譯後生成了Test.class,此時我們的Test類的Class物件就儲存在class檔案中。當我們new一個新物件或者引用靜態成員變數時,Java虛擬機器(JVM)中的類載入器子系統會將對應Class物件載入到JVM中,然後JVM再根據這個型別資訊相關的Class物件建立我們需要例項物件或者提供靜態變數的引用值。需要特別注意的是,手動編寫的每個class類,無論建立多少個例項物件,在JVM中都只有一個Class物件,即在記憶體中每個類有且只有一個相對應的Class物件。

Test t1 = new Test();
Test t2 = new Test();
Test t3 = new Test();
複製程式碼

如上所示,實際上JVM記憶體中只存有一個Test的Class物件。

Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中。Class類的例項表示java應用執行時的類(class ans enum)或介面(interface and annotation)(每個java類執行時都在JVM裡表現為一個class物件,可通過類名.class、型別.getClass()、Class.forName("類名")等方法獲取class物件)。陣列同樣也被對映為為class 物件的一個類,所有具有相同元素型別和維數的陣列都共享該 Class 物件。基本型別boolean,byte,char,short,int,long,float,double和關鍵字void同樣表現為 class 物件。

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才能呼叫建立Class物件
     * 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物件,其表示的是建立的類的型別資訊,而且這個Class物件儲存在同名.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物件),同時也就可以被用來建立這個類的所有例項物件。

java基礎:深入理解Class物件與反射機制

類載入的過程 :
1. 載入
在載入階段,虛擬機器需要完成3件事:
(1)通過一個類的全限定名(org/fenixsoft/clazz/TestClass)獲取定義此類的二進位制位元組流(.class檔案);
(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
(3)在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這個類的各種資料的訪問入口;
2. 驗證
驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機器是否能承受惡意程式碼的攻擊,從執行效能的角度上講,驗證階段的工作量在虛擬機器的類載入子系統中又佔了相當大的一部分。驗證階段大致上完成下面4個階段的驗證動作:
(1)檔案格式驗證
驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理;
這階段的驗證是基於二進位制位元組流進行的,只有通過了這個階段的驗證,位元組流才會進入記憶體的方法區進行儲存,所以後面的3個驗證階段全部是基於方法區的儲存結構進行的,不會再直接操作位元組流。
(2)後設資料驗證
對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求,保證不存在不符合Java語言規範的後設資料資訊;
(3)位元組碼驗證
通過資料流和控制流分析,確定程式是語義是合法的、符合邏輯的,保證被校驗的方法在執行時不會做出危害虛擬機器安全的事件;
(4)符號引用驗證
可以看作是對類自身以外(常量池中各種符號引用)的資訊進行匹配性校驗,確保解析動作能正常執行;
3. 準備
準備階段是正式為類變數分配記憶體並設定類變數初始值階段,這些變數所使用的記憶體都將在方法區中進行分配。這裡進行記憶體分配僅僅是類變數(被static修飾的變數),而不包括例項變數,例項變數將在物件例項化時隨著物件一起分配在Java堆中;
4. 解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行;
5. 初始化
初始化階段才真正開始執行類中定義的Java程式程式碼(或者說是位元組碼)。初始化是如何被觸發的:
(1)遇到new、getstatic、putstatic或involestatic這4條指令時;
(2)使用 java.lang.reflect 包的方法對類進行反射呼叫的時候;
(3)初始化一個類時,如果父類還沒被初始化,則先觸發父類的初始化;
(4)虛擬機器啟動時,使用者需要指定一個要執行的主類 (包含main()方法的那個類),虛擬機器會先初始化這個主類;
(5)如果一個 java.lang.invoke.MethodHandle 例項最後解析的結果是 REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,若控制程式碼所對應的類沒有進行過初始化,則將它初始化;

上文源自《深入理解java虛擬機器》一書,大家可以去讀一下,這本書基本上是java程式猿學習必讀之一了。在此就不深入展開了,因為這又是另一個JVM領域了。 以後如果寫了該方面的文章,會貼到這裡。

Class物件的獲取

Class物件的獲取主要有3種:

  • 通過例項getClass()方法獲取
  • Class.forName方法獲取
  • 類字面常量獲取

通過例項getClass()方法獲取

    Test t1 = new Test();
    Class clazz=test.getClass();
    System.out.println("clazz:"+clazz.getName());
複製程式碼

getClass()是從頂級類Object繼承而來的,它將返回表示該物件的實際型別的Class物件引用。

Class.forName方法獲取

    try{
      //通過Class.forName獲取Test類的Class物件
      Class clazz=Class.forName("com.hiway.Test");
      System.out.println("clazz:"+clazz.getName());
    }catch (ClassNotFoundException e){
      e.printStackTrace();
    }
複製程式碼

forName方法是Class類的一個static成員方法,記住所有的Class物件都源於這個Class類,因此Class類中定義的方法將適應所有Class物件。這裡通過forName方法,我們可以獲取到Test類對應的Class物件引用。 注意呼叫forName方法時需要捕獲一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字串對應的類是否存在的(是否有.class檔案),只能在程式執行時進行檢查,如果不存在就會丟擲ClassNotFoundException異常。

使用forName方式會觸發類的初始化,與之相比的是使用類字面常量獲取

類字面常量獲取

//字面常量的方式獲取Class物件
Class clazz = Test.class;
複製程式碼

這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且它根除了對forName()方法的呼叫,所以也更高效。 注意,有一點很有趣,當使用“.class”來建立對Class物件的引用時,不會自動地初始化該Class物件。注意,有一點很有趣,當使用“.class”來建立對Class物件的引用時,不會自動地初始化該Class物件,為了使用類而做的準備工作實際包含三個步驟:

  1. 載入,這是由類載入器執行的,該步驟將查詢位元組碼(通常在classpath所指定的路徑中查詢,但這並非是必需的),並從這些位元組碼中建立一個Class物件。
  2. 連結。在連結階段將驗證類中的位元組碼,為靜態域分佈儲存空間,並且如果必需的話,將解析這個類建立的對其他類的所有引用。
  3. 初始化。如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。
class Initable{
     static final int staticFinal = 47;
     static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
     static {
          System.out.ptintln("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 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);
          Clas initable3 = Class.forName("Initable3");
          System.out.println("After creating Initable3 ref");
          System.out.println(Initable3.staticNonFinal);
     }
}

/* output
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable ref
74
複製程式碼

如果一個static final值是編譯器常量,就像Initable.staticFinal那樣,那麼這個值不需要對Initable類進行初始化就可以被讀取。但是,如果只是將一個域設定為static和final的,還不足以確保這種行為,例如,對Initable.staticFinal2的訪問將強制進行類的初始化,因為它不是一個編譯期常量。

如果一個static域不是final的, 那麼在對它訪問時,總是要求在它被讀取之前,要先進行連結(為這個域分配儲存空間)和初始化(初始化該儲存空間),就像在對Initable2.staticNonFinal的訪問中所看到的那樣。從輸出結果來看,可以發現,通過字面常量獲取方式獲取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物件,肯定會觸發初始化,這點在前面已分析過。

instanceof與Class的等價性

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

public void cast2(Object obj){
    if(obj instanceof Animal){
          Animal animal= (Animal) obj;
      }
}
複製程式碼

而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;
        }
  }
複製程式碼

事實上instanceOf 與isInstance方法產生的結果是相同的。

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

/* output
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
複製程式碼

反射

反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性,這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為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 getConstructor(Class<?>... parameterTypes) 返回指定引數型別、具有public訪問許可權的建構函式物件
Constructor<?>[] getConstructors() 返回所有具有public訪問許可權的建構函式的Constructor物件陣列
Constructor getDeclaredConstructor(Class<?>... parameterTypes) 返回指定引數型別、所有宣告的(包括private)建構函式物件
Constructor<?>[] getDeclaredConstructor() 返回所有宣告的(包括private)建構函式物件
T newInstance() 呼叫無參構造器建立此 Class 物件所表示的類的一個新例項。

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

public class ConstructionTest implements Serializable {
    public static void main(String[] args) throws Exception {

        Class<?> clazz = null;

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

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

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

        //獲取帶String引數的public建構函式
        Constructor cs1 =clazz.getConstructor(String.class);
        //建立User
        User user1= (User) cs1.newInstance("hiway");
        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,"hiway2");
        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;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

/* output 
User{age=20, name='Jack'}
--------------------------------------------
user1:User{age=22, name='hiway'}
--------------------------------------------
user2:User{age=25, name='hiway2'}
--------------------------------------------
建構函式[0]:private com.example.javabase.User(int,java.lang.String)
引數型別[0]:(int,java.lang.String)
建構函式[1]:public com.example.javabase.User(java.lang.String)
引數型別[1]:(java.lang.String)
建構函式[2]:public com.example.javabase.User()
引數型別[2]:()

複製程式碼

關於Constructor類本身一些常用方法如下(僅部分,其他可查API)

方法返回值 方法名稱 方法說明
Class 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());

/* output 
-----getDeclaringClass-----
構造方法的類:com.example.javabase.User
-----getGenericParameterTypes-----
引數名稱tp:int
引數名稱tp:class java.lang.String
-----getParameterTypes-----
引數名稱:int
引數名稱:java.lang.String
-----getName-----
getName:com.example.javabase.User
-----getoGenericString-----
getoGenericString():private com.example.javabase.User(int,java.lang.String)
複製程式碼

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的欄位,包含繼承欄位

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

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方法
}
複製程式碼

上述方法需要注意的是,如果我們不期望獲取其父類的欄位,則需使用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("score");
//設定可訪問,score是private的
scoreField.setAccessible(true);
scoreField.set(st,88);
System.out.println(st.toString());

//輸出結果:Student{age=18, name='Lily ,desc='I am student', score=88} 

//獲取欄位值
System.out.println(scoreField.get(st));
// 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欄位是安全的,在執行時可以接收任何修改,但最終其實際值是不會發生改變的。

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 方法。

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

import java.lang.reflect.Method;

public class ReflectMethod  {


    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {

        Class clazz = Class.forName("reflect.Circle");

        //根據引數獲取public的Method,包含繼承自父類的方法
        Method method = clazz.getMethod("draw",int.class,String.class);

        System.out.println("method:"+method);

        //獲取所有public的方法:
        Method[] methods =clazz.getMethods();
        for (Method m:methods){
            System.out.println("m::"+m);
        }

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

        //獲取當前類的方法包含private,該方法無法獲取繼承自父類的method
        Method method1 = clazz.getDeclaredMethod("drawCircle");
        System.out.println("method1::"+method1);
        //獲取當前類的所有方法包含private,該方法無法獲取繼承自父類的method
        Method[] methods1=clazz.getDeclaredMethods();
        for (Method m:methods1){
            System.out.println("m1::"+m);
        }
    }

/**
     輸出結果:
     method:public void reflect.Shape.draw(int,java.lang.String)

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

     =========================================
     method1::private void reflect.Circle.drawCircle()

     m1::public int reflect.Circle.getAllCount()
     m1::private void reflect.Circle.drawCircle()
     */
}

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;
    }
}
複製程式碼

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

Class clazz = Class.forName("reflect.Circle");
//建立物件
Circle circle = (Circle) clazz.newInstance();

//獲取指定引數的方法物件Method
Method method = clazz.getMethod("draw",int.class,String.class);

//通過Method物件的invoke(Object obj,Object... args)方法呼叫
method.invoke(circle,15,"圈圈");

//對私有無參方法的操作
Method method1 = clazz.getDeclaredMethod("drawCircle");
//修改私有方法的訪問標識
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中新增一個預設的方法實現,返回的就引數型別資訊

public interface Type {
    //1.8新增
    default String getTypeName() {
        return toString();
    }
}
複製程式碼

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

相關文章