Java 反射基礎

zeroXuan發表於2019-03-30

一、什麼是反射

反射 (Reflection) 是 Java 的特徵之一,它允許執行中的 Java 程式獲取自身的資訊,並且可以操作類或物件的內部屬性。

總而言之,通過反射,我們可以在執行時獲得程式或程式集中每一個型別的成員和成員的資訊。程式中一般的物件的型別都是在編譯期就確定下來的,而 Java 反射機制可以動態地建立物件並呼叫其屬性,這樣的物件的型別在編譯期是未知的。

反射的核心是 JVM 在執行時才動態載入類或呼叫方法/訪問屬性,它不需要事先(寫程式碼的時候或編譯期)知道執行物件是誰。

1.1 反射機制主要提供以下功能

  • 執行時判斷任意一個物件所屬的類。
  • 執行時構造任意一個類的物件。
  • 執行時判斷任意一個類所具有的成員變數和方法。
  • 執行時呼叫任意一個物件的方法

當我們的程式在執行時,需要動態的載入一些類,這些類可能之前用不到所以不用載入到jvm,而是在執行時根據需要才載入。

例如我們的專案底層有時是用mysql,有時用oracle,需要動態地根據實際情況載入驅動類,這個時候反射就有用了,假設 com.java.dbtest.mySqlConnection,com.java.dbtest.oracleConnection這兩個類我們要用,這時候我們的程式就寫得比較動態化,通過Class tc = Class.forName("com.java.dbtest.mySqlConnection");通過類的全類名讓jvm在伺服器中找到並載入這個類,而如果是oracle則傳入的引數就變成另一個了。這時候就可以看到反射的好處了,這個動態性就體現出java的特性了!

二、反射的主要用途

很多人都認為反射在實際的 Java 開發應用中並不廣泛,其實不然。當我們在使用 IDE時,我們輸入一個物件或類並想呼叫它的屬性或方法時,一按點號,編譯器就會自動列出它的屬性或方法,這裡就會用到反射。

反射最重要的用途就是開發各種通用框架。很多框架(比如 Spring)都是配置化的(比如通過 XML 檔案配置 Bean),為了保證框架的通用性,它們可能需要根據配置檔案載入不同的物件或類,呼叫不同的方法,這個時候就必須用到反射,執行時動態載入需要載入的物件。

三、反射的基本運用

上面我們提到了反射可以用於判斷任意物件所屬的類,獲得Class物件,構造任意一個物件以及呼叫一個物件。

3.1 Java 的反射機制的實現藉助於4個類:class,Constructor,Field,Method;

其中class代表的是類物件,Constructor-類的構造器物件,Field-類的屬性物件,Method-類的方法物件,通過這四個物件我們可以粗略的看到一個類的各個組成部分。其中最核心的就是Class類,它是實現反射的基礎,它包含的方法我們在第一部分已經進行了基本的闡述。應用反射時我們最關心的一般是一個類的構造器、屬性和方法,下面我們主要介紹Class類中針對這三個元素的方法:

3.2 獲得Class物件

Class類的例項表示Java應用執行時的類(class and enum)或介面(interface and annotation)(每個Java類執行時都在JVM裡表現為一個Class物件,可通過類名.class,型別.getClass(),Class.forName("類名")等方法獲取Class物件)。基本型別boolean,byte,char,short,int,long,float,double和關鍵字void同樣表現為Class物件。

宣告普通的Class物件,在編譯器並不會檢查Class物件的確切型別是否符合要求,如果存在錯誤只有在執行時才得以暴露出來。但是通過泛型宣告指明型別的Class物件,編譯器在編譯期將對帶泛型的類進行額外的型別檢查,確保在編譯期就能保證型別的正確性。

使用 Class 類的 forName 靜態方法:

public static Class<?> forName(String className)

比如在 JDBC 開發中常用此方法載入資料庫驅動:
Class.forName(driver);
複製程式碼

直接獲取某一個物件的 class

Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;

複製程式碼

呼叫某個物件的 getClass() 方法

StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();
複製程式碼

3.3 判斷是否為某個類的例項

一般地,我們用 instanceof 關鍵字來判斷是否為某個類的例項。同時我們也可以藉助反射中 Class 物件的 isInstance() 方法來判斷是否為某個類的例項,它是一個 native 方法:

public native boolean isInstance(Object obj);
複製程式碼

3.4 建立例項

使用Class物件的newInstance()方法來建立Class物件對應類的例項

classObj.newInstance() 只能夠呼叫public型別的無參建構函式,此方法是過時的

Class<?> c = String.class;
Object str = c.newInstance();
複製程式碼

先通過Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法來建立例項。

可以根據傳入的引數,呼叫任意建構函式,在特定情況下,可以呼叫私有的建構函式,此方法是推薦使用的

//獲取String所對應的Class物件
Class<?> c = String.class;
//獲取String類帶一個String引數的構造器
Constructor constructor = c.getConstructor(String.class);
//根據構造器建立例項
Object obj = constructor.newInstance("23333");
System.out.println(obj);
複製程式碼

3.5 獲取方法

getMethods()

返回某個類的所有public方法,包括自己宣告和從父類繼承的

getDeclaredMethods()

獲取所有本類自己的方法,不問訪問許可權,不包括從父類繼承的方法

getMethod(String name, Class<?>... parameterTypes)

方法返回一個特定的方法,其中第一個引數為方法名稱,後面的引數為方法的引數對應Class的物件。

Method getDeclaredMethod(String name, Class<?>... params)

方法返回一個特定的方法,其中第一個引數為方法名稱,後面的引數為方法的引數對應Class的物件

操作私有方法

/**
 * 訪問物件的私有方法
 * 為簡潔程式碼,在方法上丟擲總的異常,實際開發別這樣
 */
private static void getPrivateMethod() throws Exception{
    //1. 獲取 Class 類例項
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 獲取私有方法
    //第一個引數為要獲取的私有方法的名稱
    //第二個為要獲取方法的引數的型別,引數為 Class...,沒有引數就是null
    //方法引數也可這麼寫 :new Class[]{String.class , int.class}
    Method privateMethod =
            mClass.getDeclaredMethod("privateMethod", String.class, int.class);

    //3. 開始操作方法
    if (privateMethod != null) {
        //獲取私有方法的訪問權
        //只是獲取訪問權,並不是修改實際許可權
        privateMethod.setAccessible(true);

        //使用 invoke 反射呼叫私有方法
        //privateMethod 是獲取到的私有方法
        //testClass 要操作的物件
        //後面兩個引數傳實參
        privateMethod.invoke(testClass, "Java Reflect ", 666);
    }
}

複製程式碼

3.6 獲取建構函式

Constructor[] getConstructors()

獲得類的所有公共建構函式

Constructor getConstructor(Class[] params)

獲得使用特殊的引數型別的公共建構函式,

Constructor[] getDeclaredConstructors()

獲得類的所有建構函式

Constructor getDeclaredConstructor(Class[] params)

獲得使用特定引數型別的建構函式

3.7 獲取成員變數欄位

Field[] getFields()

獲得類的所有公共欄位

Field getField(String name)

獲得命名的公共欄位

Field[] getDeclaredFields()

獲得類宣告的所有欄位

Field getDeclaredField(String name)

獲得類宣告的命名的欄位

修改私有變數

**
 * 修改物件私有變數的值
 * 為簡潔程式碼,在方法上丟擲總的異常
 */
private static void modifyPrivateFiled() throws Exception {
    //1. 獲取 Class 類例項
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 獲取私有變數
    Field privateField = mClass.getDeclaredField("MSG");

    //3. 操作私有變數
    if (privateField != null) {
        //獲取私有變數的訪問權
        privateField.setAccessible(true);

        //修改私有變數,並輸出以測試
        System.out.println("Before Modify:MSG = " + testClass.getMsg());

        //呼叫 set(object , value) 修改變數的值
        //privateField 是獲取到的私有變數
        //testClass 要操作的物件
        //"Modified" 為要修改成的值
        privateField.set(testClass, "Modified");
        System.out.println("After Modify:MSG = " + testClass.getMsg());
    }
}
複製程式碼

3.8 呼叫方法

當我們從類中獲取了一個方法後,我們就可以用 invoke() 方法來呼叫這個方法。invoke 方法的原型為:

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
複製程式碼

下面是一個例項

public class test1 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> klass = methodClass.class;
        //建立methodClass的例項
        Object obj = klass.newInstance();
        //獲取methodClass類的add方法
        Method method = klass.getMethod("add",int.class,int.class);
        //呼叫method對應的方法 => add(1,4)
        Object result = method.invoke(obj,1,4);
        System.out.println(result);
    }
}
class methodClass {
    public final int fuck = 3;
    public int add(int a,int b) {
        return a+b;
    }
    public int sub(int a,int b) {
        return a+b;
    }
}
複製程式碼

3.9 利用反射建立陣列

陣列在Java裡是比較特殊的一種型別,它可以賦值給一個Object Reference。下面我們看一看利用反射建立陣列的例子:

public static void testArray() throws ClassNotFoundException {
        Class<?> cls = Class.forName("java.lang.String");
        Object array = Array.newInstance(cls,25);
        //往陣列裡新增內容
        Array.set(array,0,"hello");
        Array.set(array,1,"Java");
        Array.set(array,2,"fuck");
        Array.set(array,3,"Scala");
        Array.set(array,4,"Clojure");
        //獲取某一項的內容
        System.out.println(Array.get(array,3));
    }
複製程式碼

其中的Array類為java.lang.reflect.Array類。我們通過Array.newInstance()建立陣列物件,它的原型是:

public static Object newInstance(Class<?> componentType, int length)
        throws NegativeArraySizeException {
        return newArray(componentType, length);
    }
複製程式碼

而 newArray 方法是一個 native 方法

private static native Object newArray(Class<?> componentType, int length)
        throws NegativeArraySizeException;
複製程式碼

3.10 反射修改常量值

判斷可不可以修改常量

相關文章