Java反射機制

w39發表於2021-09-09

一、Java反射機制概述

Java放射機制是指在==執行狀態==中,對於任意一個類,都能知道這個類的所有屬性和方法;對於任意一個物件,都能呼叫它的任意一個方法和屬性;這種動態獲取資訊及動態呼叫方法的功能成為Java的反射機制

二、反射的作用

  1. 利用Java機制,在Java程式中可以動態的去呼叫一些protected甚至是private的方法或類,這樣就可以在很大程度上滿足一些特殊需求。
  2. Android SDK的原始碼中,很多類或方法中經常加上了“@hide”註釋標記,它的作用是使這個方法或類在生成SDK時不可見,所以程式可能無法編譯通過,而且在最終釋出的時候,就可能存在一些問題。對於這種問題,第一種方法就是自己去掉原始碼中的“@hide”標記,然後在重新編譯生成一個SDK。另一種方法就是使用Java反射機制,利用反射機制可以訪問存在訪問許可權的方法或修改其訪問域。
  3. 可能有人會有疑問,明明直接new物件就好了,為什麼非要用反射呢?程式碼量不是反而增加了?其實反射的初衷不是方便你去建立一個物件,而是讓你在寫程式碼的時候可以更加靈活,降低耦合,提高程式碼的自適應能力。

怎麼樣降低耦合度,提高程式碼的自適應能力?

通過介面實現,但是介面如果需要用到new關鍵字,這時候耦合問題又會出現

舉個例子:


/**
/**
 * @ClassName: TestReflection 
 * @Model : (所屬模組名稱)
 * @Description: 使用反射降低耦合度(通過介面來實現)
 * @author Administrator 
 * @date 2017年6月2日 下午5:09:38 
 */
public class TestReflection {

    /**
    /**
     * main (這裡用一句話描述這個方法的作用) 
     * @param args
     * void 
     * @ModifiedPerson Administrator
     * @date 2017年6月2日 下午5:09:38 
     */
    public static void main(String[] args) {

        //普通寫法,使用New 關鍵字
        ITest iTest = createITest("ITestImpl1");
        iTest.testReflect();
        ITest iTest2 = createITest("ITestImpl2");
        iTest2.testReflect();
        
    }

    /**
    * createITest 普通寫法,使用New關鍵字,但是假設有1000個不同ITest需要建立,那你打算寫1000個 if語句來返回不同的ITest物件?
    * @param name
    * @return
    * ITest 
    * @ModifiedPerson Administrator
    * @date 2017年6月2日 下午5:32:21
     */
    public static ITest createITest(String name){
        
        if (name.equals("ITestImpl1")) {
            return new ITestImpl1();
        } else if(name.equals("ITestImpl2")){
            return new ITestImpl2();
        }
    
        return null;
    }

}


interface ITest{
    public void testReflect();
}

class ITestImpl1 implements ITest{

    /* (non-Javadoc)
    * <p>Title: test</p> 
    * <p>Description: </p>  
    * @see ITest#test()
    */
    @Override
    public void testReflect() {

         System.out.println("I am ITestImpl1 !");
    }
}

class ITestImpl2 implements ITest{

    /* (non-Javadoc)
    * <p>Title: testReflect</p> 
    * <p>Description: </p>  
    * @see ITest#testReflect()
    */
    @Override
    public void testReflect() {

        System.out.println("I am ITestImpl2 !");
    }
    
}


複製程式碼

假設有1000個不同ITest需要建立,那你打算寫1000個 if語句來返回不同的ITest物件?

如果使用反射機制呢?


/**
/**
 * @ClassName: TestReflection 
 * @Model : (所屬模組名稱)
 * @Description: 使用反射降低耦合度(通過介面來實現)
 * @author Administrator 
 * @date 2017年6月2日 下午5:09:38 
 */
public class TestReflection {

    /**
    /**
     * main (這裡用一句話描述這個方法的作用) 
     * @param args
     * void 
     * @ModifiedPerson Administrator
     * @date 2017年6月2日 下午5:09:38 
     */
    public static void main(String[] args) {
        //普通寫法,使用New 關鍵字
        ITest iTest = createITest("ITestImpl1");
        iTest.testReflect();
        ITest iTest2 = createITest("ITestImpl2");
        iTest2.testReflect();
        
        //使用反射機制
        ITest iTest3 = createITest2("ITestImpl1");
        iTest3.testReflect();
        ITest iTest4 = createITest2("ITestImpl2");
        iTest4.testReflect();
        
    }

    /**
    * createITest 普通寫法,使用New關鍵字,但是假設有1000個不同ITest需要建立,那你打算寫1000個 if語句來返回不同的ITest物件?
    * @param name
    * @return
    * ITest 
    * @ModifiedPerson Administrator
    * @date 2017年6月2日 下午5:32:21
     */
    public static ITest createITest(String name){
        
        if (name.equals("ITestImpl1")) {
            return new ITestImpl1();
        } else if(name.equals("ITestImpl2")){
            return new ITestImpl2();
        }
    
        return null;
    }
    
    /**
    * createITest2 使用反射機制:當有1000個不同ITest需要建立時,不用針對每個建立ITest物件
    * @param name
    * @return
    * ITest 
    * @ModifiedPerson Administrator
    * @date 2017年6月2日 下午5:34:55
     */
    public static ITest createITest2(String name){
        try {
            Class<?> class1 = Class.forName(name);
            ITest iTest = (ITest) class1.newInstance();
            return iTest;
        } catch (ClassNotFoundException e) {

            e.printStackTrace();
        } catch (InstantiationException e) {

            e.printStackTrace();
        } catch (IllegalAccessException e) {

            e.printStackTrace();
        }
        
        return null;
        
    }

}


interface ITest{
    public void testReflect();
}

class ITestImpl1 implements ITest{

    /* (non-Javadoc)
    * <p>Title: test</p> 
    * <p>Description: </p>  
    * @see ITest#test()
    */
    @Override
    public void testReflect() {

         System.out.println("I am ITestImpl1 !");
    }
}

class ITestImpl2 implements ITest{

    /* (non-Javadoc)
    * <p>Title: testReflect</p> 
    * <p>Description: </p>  
    * @see ITest#testReflect()
    */
    @Override
    public void testReflect() {

        System.out.println("I am ITestImpl2 !");
    }
    
}


複製程式碼

利用反射機制進行解耦的原理:就是利用反射機制"動態"的建立物件:向createITest()方法傳入Hero類的包名.類名 通過載入指定的類,然後再例項化物件.

三、理解Class類和類型別

要想理解反射,首先要理解Class類,因為Class類是反射實現的基礎。

3.1 類是物件嗎

在物件導向的世界裡,萬物皆物件,物件是一個類的例項,所以類是java.lang.Class類的例項物件,而Class是所有類的類(This is a class named Class),Class類是類型別,即類的型別。

3.2 Class的物件的獲取

對於普通的物件,我們都是直接通過new來建立一個物件。

Student sdutent = new Student();

但是Class的物件是類,不能通過new來建立。Class的原始碼描述如下:

/*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * 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;
    }
複製程式碼

構造器是私有的,只有Java虛擬機器(JVM)可以建立Class的物件,不能像普通類一樣new一個Class物件。只能是通過已有的類來得到一個Class物件(三種方法):

第一種方法
Class<?> cls = Class.forName("com.example.student");//forName(包名.類名)
Student s1 = (Student)cls.newInstance();
複製程式碼
  1. 通過JVM查詢並載入指定的類
  2. 呼叫newInstance()方法讓載入完的類在記憶體中建立對應的例項,並把例項賦值給s1
第二種方法
Student s = new Student;    
Class<?> cls = s.getClass;
Student s2 = (Student)cls.newInstance();
複製程式碼
  1. 在記憶體中新建一個Student的例項,物件s對這個記憶體地址進行引用
  2. 物件s呼叫getClass()方法返回物件s所對應的Class物件
  3. 呼叫newInstance()方法讓Class物件在記憶體中建立物件的例項,並讓s2引用例項的記憶體地址
第三種方法
Class<?> cls = Student.Class();
Student s3 = (Student)cls.newInstance();
複製程式碼
  1. 獲取指定型別的Class物件,這裡是Student
  2. 呼叫newInstance()方法讓Class物件在記憶體中建立對應例項,並讓s3引用例項的記憶體地址
注意:
  • cls.newInstance()方法返回的是一個泛型T,我們要強轉成Student類
  • cls.newInstance()預設返回的是Student類的無引數構造物件
  • 被反射機制載入的類必須有無引數構造方法,否者執行會丟擲異常

通過類型別建立類和通過new建立類的不同之處是:類型別建立的是動態載入類。

四、動態載入類

程式執行分為編譯器和執行期,編譯時刻載入一個類就稱為靜態載入類,執行時刻載入類稱為動態載入類,下面通過一個例項來講解:

現在拋開IDE工具,用記事本手寫類,這是為了方便我們利用cmd命令列手動編譯和執行一個類,從而更好理解動態載入類和靜態載入類的區別。

首先寫First.java

class First
{
    public static void main(String[] args)
    {
        if ("Word".equals(args[0]))
        {   
            // 靜態載入類,在編譯時載入
            Word w = new Word();
            w.start();
        }
        if ("Excel".equals(args[0]))
        {
            Excel e = new Excel();
            e.start();
        }
    }
}
複製程式碼

然後進入cmd編譯First.java

由於我們new的兩個類Word和Excel沒有編譯,所以報錯了,這就是靜態載入類的缺點,即必須在編譯時期就載入所有可能用到的類,而我們希望實現的是執行時用到哪個類就載入哪個類,下面通過動態載入類來加以改進。

改進以後的類:FirstBetter.java

class FirstBetter
{   
    public static void main(String[] args)
    {
            try
        {
            // 動態載入類,在執行時載入
            Class c = Class.forName(args[0]);
            // 通過類型別,建立該類物件
            FirstAble oa = (FirstAble)c.newInstance();
            oa.start();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }   
}
複製程式碼

這裡動態載入了名為args[0]的類,而args[0]是在執行期輸入給main方法的第一個引數,如果你輸入Word那麼就會載入Word.java,這時候就需要在與FirstBetter.java相同路徑下面建立Word.java;同理,如果你輸入Excel就需要載入Excel.java了。 其中FirstAble是一個介面,上面動態載入的類如Word、Excel就是實現了FirstAble,體現了多型的思想,這種動態載入和多型的思想可以使具體功能和程式碼解耦,也就是隨時想新增某個功能(如Word和Excel都不要了,我要PPT)都能動態新增,而不改動原來的程式碼。

其中FirstAble介面如下:

interface FirstAble
{
    public void start();
}
複製程式碼

Word類:

class Word implements FirstAble
{
    public void start()
    {
        System.out.println("word...starts...");
    }
}
複製程式碼

按順序編譯、執行上面的類。

五、獲取類的資訊

一個類中通常包含屬性和方法,通過反射獲取類的構造方法、成員方法、成員變數、修飾(方法和變數的)等。

5.1 獲取類的建構函式

建構函式中都包括什麼:建構函式引數

類的成建構函式是一個物件,它是java.lang.reflect.Constructor的一個物件,所以我們通過java.lang.reflect.Constructor裡面封裝的方法來獲取這些資訊。

1、單獨獲取某個建構函式

通過Class類的以下方法實現:

  • public Constructor getDeclaredConstructor(Class<?>... parameterTypes) // 根據建構函式的引數,返回一個具體的建構函式(不分public和非public屬性)
  • public Constructor getConstructor(Class<?>... parameterTypes) // 根據建構函式的引數,返回一個具體的具有public屬性的建構函式
  • 引數parameterTypes為建構函式引數類的類型別列表。

例如類A有如下一個建構函式:

public A(String a, int b) {
    // code body
}
複製程式碼

那麼就可以通過:

Constructor constructor = a.getDeclaredConstructor(String.class, int.class);
複製程式碼

來獲取這個建構函式。

2、獲取所有的建構函式

通過Class類的以下方法實現:

  • Constructor getDeclaredConstructors() 返回該類中所有的建構函式陣列(不分public和非public屬性)
  • Constructor getConstructors() 返回所有具有public屬性的建構函式陣列

可以通過以下步驟實現:

1、已知一個物件,獲取其類的類型別

Class c = obj.getClass();
複製程式碼

2、獲取該類的所有建構函式,放在一個陣列中

Constructor[] constructors = c.getDeclaredConstructors();
複製程式碼

3、遍歷建構函式陣列,獲得某個建構函式constructor

for (Constructor constructor : constructors)
複製程式碼

4、得到建構函式引數型別的類型別陣列

Class[] paramTypes = constructor.getParameterTypes();
複製程式碼

5、遍歷引數類的類型別陣列,得到某個引數的類型別class1

for (Class class1 : paramTypes)
複製程式碼

6、得到該引數的型別名

String paramName = class1.getName();
複製程式碼

例子:

private static void forName2GetConstructor() {
        try {
            //第一種方法:forName()
            Class<?> class1 = Class.forName("com.zyt.reflect.StudentInfo");
            
            Constructor<?>[] constructors = class1.getDeclaredConstructors();
            for (Constructor<?> constructor : constructors) {
                System.out.print("構造方法:"+constructor.getName()+" 引數型別:");
                Class<?>[] parameterTypes = constructor.getParameterTypes();
                for (Class<?> class2 : parameterTypes) {
                    System.out.print(class2.getName()+" , ");
                }
                System.out.print("\n");
            }
            
            //呼叫構造方法
            Constructor<?> constructor = class1.getDeclaredConstructor(String.class, String.class, int.class);
            StudentInfo instance = (StudentInfo) constructor.newInstance("張三", "男", 18);
            System.out.println("呼叫構造方法 == "+instance.getName()+","+instance.getScore());
            
            
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
複製程式碼

5.2 獲取類的成員方法

成員方法中都包括什麼:返回值型別+方法名+引數型別

在Java中,類的成員方法也是一個物件,它是java.lang.reflect.Method的一個物件,所以我們通過java.lang.reflect.Method裡面封裝的方法來獲取這些資訊.

1、單獨獲取某一個方法
  • Method getMethod(String name, Class[] params) 根據方法名和引數,返回一個具體的具有public屬性的方法
  • Method getDeclaredMethod(String name, Class[] params) 根據方法名和引數,返回一個具體的方法(不分public和非public屬性)
  • 兩個引數分別是方法名和方法引數類的類型別列表。

例如類A有如下一個方法:

public void print(String a, int b) {
    // code body
}
複製程式碼

現在知道A有一個物件a,那麼就可以通過:

Class c = a.getClass();
Method method = c.getDeclaredMethod("print", String.class, int.class);
複製程式碼

來獲取這個方法。

如何呼叫獲取到的方法

那得到方法以後如何呼叫這個方法呢,通過Method類的以下方法實現:

public Object invoke(Object obj, Object… args)
複製程式碼

兩個引數分別是這個方法所屬的物件和這個方法需要的引數,還是用上面的例子來說明,通過:

method.invoke(a, "hello", 10);
複製程式碼

和通過普通呼叫:

a.print("hello", 10);
複製程式碼

效果完全一樣,這就是方法的反射,invoke()方法可以反過來將其物件作為引數來呼叫方法,完全跟正常情況反了過來。

2、獲取類中所有成員方法的資訊
  • Method[] getMethods() 返回所有具有public屬性的方法陣列
  • Method[] getDeclaredMethods() 返回該類中的所有的方法陣列(不分public和非public屬性)
注意:

getMethods():用於獲取類的所有的public修飾域的成員方法,包括從父類繼承的public方法和實現介面的public方法;

getDeclaredMethods():用於獲取在當前類中定義的所有的成員方法和實現的介面方法,不包括從父類繼承的方法。

大家可以查考一下開發文件的解釋:

getMethods() - Returns an array containing Method objects for all public methods for the class C represented by this Class. 

&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; Methods may be declared in C, the interfaces it implements or in the superclasses of C. 

&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; The elements in the returned array are in no particular order. 

getDeclaredMethods() - Returns a Method object which represents the method matching the specified name and parameter types 

&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;  that is declared by the class represented by this Class.
複製程式碼

因此在示例程式碼的方法get_Reflection_Method(...)中,ReflectionTest類繼承了Object類,實現了actionPerformed方法,並定義如下成員方法:

image
通過這兩個語句執行後的結果不同:

  a、Method[] methods = temp.getDeclaredMethods()執行後結果如下:

   

image

  b、Method[] methods = temp.getMethods()執行後,結果如下:

  

image
   

Method類:

public invoke( obj,                       ... args)
複製程式碼

對帶有指定引數的指定物件呼叫由此 Method物件表示的底層方法。個別引數被自動解包,以便與基本形參相匹配,基本引數和引用引數都隨需服從方法呼叫轉換。

  • 如果底層方法是靜態的,那麼可以忽略指定的 obj引數。該引數可以為 null。

  • 如果底層方法所需的形引數為 0,則所提供的 args陣列長度可以為 0 或 null。

  • 如果底層方法是例項方法,則使用動態方法查詢來呼叫它,這一點記錄在 Java Language Specification, Second Edition 的第 15.12.4.4 節中;在發生基於目標物件的執行時型別的重寫時更應該這樣做。

  • 如果底層方法是靜態的,並且尚未初始化宣告此方法的類,則會將其初始化。

  • 如果方法正常完成,則將該方法返回的值返回給呼叫者;如果該值為基本型別,則首先適當地將其包裝在物件中。但是,如果該值的型別為一組基本型別,則陣列元素不被包裝在物件中;換句話說,將返回基本型別的陣列。如果底層方法返回型別為 void,則該呼叫返回 null。

引數:

obj- 從中呼叫底層方法的物件

args- 用於方法呼叫的引數

返回:

使用引數 args在 obj上指派該物件所表示方法的結果   如果想要獲得類中所有而非單獨某個成員方法的資訊,可以通過以下幾步來實現:

1、已知一個物件,獲取其類的類型別

Class c = obj.getClass();
複製程式碼

2、獲取該類的所有方法,放在一個陣列中

Method[] methods = c.getDeclaredMethods();
複製程式碼

3、遍歷方法陣列,獲得某個方法method

for (Method method : methods)
複製程式碼

4、得到方法返回值型別的類型別

Class returnType = method.getReturnType();
複製程式碼

5、得到方法返回值型別的名稱

String returnTypeName = returnType.getName();
複製程式碼

6、得到方法的名稱

String methodName = method.getName();
複製程式碼

7、得到所有引數型別的類型別陣列

Class[] paramTypes = method.getParameterTypes();
複製程式碼

8、遍歷引數類的類型別陣列,得到某個引數的類型別class1

for (Class class1 : paramTypes)
複製程式碼

9、得到該引數的型別名

String paramName = class1.getName();
複製程式碼

例子:

private static void getClass2GetMethod(){
        StudentInfo studentInfo = new StudentInfo();
        Class<? extends StudentInfo> class1 = studentInfo.getClass();
        
        Method[] methods = class1.getDeclaredMethods();
        for (Method method : methods) {
            System.out.print("方法名 : "+method.getName());
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (Class<?> class2 : parameterTypes) {
                System.out.print(" 引數型別: "+class2.getName());
            }
            Class<?> returnType = method.getReturnType();
            System.out.println(" 返回值型別: "+returnType.getName());
        }
        
        try {
            //1、呼叫非靜態方法
            Method method1 = class1.getDeclaredMethod("setName", String.class);
            Method method2 = class1.getDeclaredMethod("getName");
            method1.invoke(studentInfo, "李四");
            String name = (String) method2.invoke(studentInfo);
            System.out.println("呼叫非靜態方法 getName()==name:"+name);
            
            //2、呼叫靜態方法:將invoke的第一個引數設定為null
            System.out.println("呼叫靜態方法 test():");
            Method method3 = class1.getDeclaredMethod("test");
            method3.invoke(null);
            Method method4 = class1.getDeclaredMethod("test", String.class);
            method4.invoke(null, "123456");
            
            //3、呼叫私有方法:方法呼叫之前,需要將方法setAccessible
            Method method5 = class1.getDeclaredMethod("getAge");
            method5.setAccessible(true);
            System.out.println("呼叫私有方法 getAge()=="+method5.invoke(studentInfo));
            
            //4、呼叫包含物件引數的方法(多個引數):
            Method method6 = class1.getDeclaredMethod("showMsg", String.class,int.class,MsgClass.class);
            //Method method6 = class1.getDeclaredMethod("showMsg", new Class[]{String.class,int.class,MsgClass.class});
            //method6.invoke(studentInfo, new Object[]{"王小五",28,new MsgClass()});
            method6.invoke(studentInfo,"趙小六",28,new MsgClass());
            
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
複製程式碼

5.3 獲取類的成員變數

成員變數中都包括什麼:成員變數型別+成員變數名

類的成員變數也是一個物件,它是java.lang.reflect.Field的一個物件,所以我們通過java.lang.reflect.Field裡面封裝的方法來獲取這些資訊。

1、單獨獲取某個成員變數
  • Field getField(String name) 根據變數名,返回一個具體的具有public屬性的成員變數
  • Field getDeclaredField(String name) 根據變數名,返回一個成員變數(不分public和非public屬性)
  • 引數是成員變數的名字。

例如一個類A有如下成員變數:

private int n;
複製程式碼

如果A有一個物件a,那麼就可以這樣得到其成員變數:

Class c = a.getClass();
Field field = c.getDeclaredField("n");
複製程式碼
2、獲取所有的成員變數
  • Field[] getFields() 返回具有public屬性的成員變數的陣列
  • Field[] getDelcaredField() 返回所有成員變數組成的陣列(不分public和非public屬性)

同樣,如果想要獲取所有成員變數的資訊,可以通過以下幾步:

1、已知一個物件,獲取其類的類型別

Class c = obj.getClass();
複製程式碼

2、獲取該類的所有成員變數,放在一個陣列中

Field[] fields = c.getDeclaredFields();
複製程式碼

3、遍歷變數陣列,獲得某個成員變數field

for (Field field : fields)
複製程式碼

4、得到成員變數型別的類型別

Class fieldType = field.getType();
複製程式碼

5、得到成員變數的型別名

String typeName = fieldType.getName();
複製程式碼

6、得到成員變數的名稱

String fieldName = field.getName();
複製程式碼

例子:

private static void class2GetField() {
        Class<?> class1 =  StudentInfo.class;
        Field[] fields = class1.getDeclaredFields();
        for (Field field : fields) {
            System.out.print("變數名 : "+field.getName());
            Class<?> type = field.getType();
            System.out.println(" 型別 : "+type.getName());
        }
        
        try {
            //訪問非私有變數
            StudentInfo studentInfo = new StudentInfo();
            Field field = class1.getDeclaredField("name");
            System.out.println(" 變數name== "+field.get(studentInfo));
            Field field2 = class1.getDeclaredField("school");
            System.out.println(" 靜態變數school== "+field2.get(studentInfo));
            
            //訪問私有變數
            Field field3 = class1.getDeclaredField("age");
            field3.setAccessible(true);
            System.out.println(" 私有變數age== "+field3.get(studentInfo));
            field3.set(studentInfo, 18);
            System.out.println(" 私有變數age== "+field3.get(studentInfo));

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
複製程式碼

5.4 獲取類、方法、屬性的修飾域

類Class、Method、Constructor、Field都有一個public方法int getModifiers()。該方法返回一個int型別的數,表示被修飾物件( Class、 Method、 Constructor、 Field )的修飾型別的組合值。

  在開發文件中,可以查閱到,Modifier類中定義了若干特定的修飾域,每個修飾域都是一個固定的int數值,列表如下:

image
    

  該類不僅提供了若干用於判斷是否擁有某中修飾域的方法boolean isXXXXX(int modifiers),還提供一個String toString(int modifier)方法,用於將一個表示修飾域組合值的int數轉換成描述修飾域的字串。

image

5.5 通過反射獲取私有成員變數和私有方法

Person類

public class Person {
private String name = "zhangsan";
private String age;
 
public String getName() {
    return name;
}
 
public void setName(String name) {
    this.name = name;
}
}  

    //main函式
 
    Person person = new Person();
    //列印沒有改變屬性之前的name值
    System.out.println("before:" + getPrivateValue(person, "name"));
    person.setName("lisi");
    //列印修改之後的name值
    System.out.println("after:" + getPrivateValue(person, "name"));
 
 
 
/**
 * 通過反射獲取私有的成員變數
 *
 * @param person
 * @return
 */
private Object getPrivateValue(Person person, String fieldName) {
 
    try {
        Field field = person.getClass().getDeclaredField(fieldName);
        // 引數值為true,開啟禁用訪問控制檢查
        //setAccessible(true) 並不是將方法的訪問許可權改成了public,而是取消java的許可權控制檢查。
        //所以即使是public方法,其accessible 屬相預設也是false
        field.setAccessible(true);
        return field.get(person);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}  
複製程式碼

執行結果:

image

獲取私有方法的方式類似獲取私有成員變數的方式 Filed類,Method類等詳細檢視開發者文件: developer.android.com/intl/zh-cn/…

六、關於反射的一些高階話題

如果說前面那些屬於Java反射的基本知識,那麼在文章的最後,我們來探討一下反射的一些高階話題。另外,本文對基礎知識的講解僅屬於抓主幹,具體的一些旁支可以自己參看文件。需要提一下的是,Java反射中對陣列做過單獨的優化處理,具體可檢視java.lang.reflect.Array類;還有關於泛型的支援,可檢視java.lang.reflect.ParameterizedType及相關資料。

暫時想到的高階話題有三個,由於對Java反射理解的也不算深入,所以僅僅從思路上進行探討,具體實現上,大家可以參考其他相關資料,做更深入研究。

Android編譯期問題

Android的安全許可權問題我把它簡單的劃分成三個層次,最不嚴格的一層就是僅僅騙過編譯器的“@hide”標記。對於一款開源的作業系統而言,這個標記本身並不具備安全上的限制。不過,從上次Google過來的負責Android工程師的說法來看,這個標記的作用更多的是方便硬體廠商做閉源的二次開發。這樣解釋倒也說得過去。

不過這並不影響我們使用反射機制以繞過原生Android的第一層安全措施。如果你熟悉原始碼的話,會發現這可以應用到很多地方。並且最關鍵的是你並不需要放在原始碼中編譯,而是像普通應用程式的開發過程一樣。

具體使用範圍我不能一一列舉了,例如自定義視窗、安裝程式等等。簡單的說,在Android上使用反射技術,你才會對Android系統有更深的理解和更高的控制權。

軟體的解耦合

我們在架構程式碼的時候,經常提到解耦合、弱耦合。其實,解耦和不僅僅只能在程式碼上做文章。我們可以考慮這樣一種情況:軟體的功能需求不可能一開始就完全確定,有一些功能在軟體開發的後期甚至是軟體已經發布出去之後才想到要加入或者去掉。 按我們慣有的思維,這種情況就得改動原始碼,重新編譯。如果軟體已經發布出去,那麼就得讓客戶重新安裝一次軟體。反思一下,我們是否認為軟體和程式是同一回事呢?事實上,如果你能將軟體和程式分開來理解,那麼你會發現,為了應對以上的情況,我們還有其他的解決辦法。

我國有一個很重要但是很麻煩的制度,那就是戶籍制度。它的本意是為了更好的管理人口事宜。每當一個孩子出生,我們就需要在戶籍管理的地方去給他辦理戶籍入戶;而每當一個人去世,我們也需要在相應的地方銷去他的戶籍。既然我們可以視類為生命,那麼我們能否通過學習這樣的戶籍管理制度來動態地管理類呢?

事實上這樣的管理是可行的,而且Java虛擬機器本身正是基於這樣的機制來執行程式的。因此我們是否可以這樣來架構軟體框架。首先,我們的軟體有一個配置檔案,配置檔案其實是一個文字,裡面詳細描述了,我們的軟體核心部分執行起來後還需要從什麼路徑載入些什麼類需要何時呼叫什麼方法等。這樣當我們需要加或減某些功能時,我們只需要簡單地修改配置文字檔案,然後刪除或者新增相應的.class檔案就可以了。

如果你足夠敏感,你或許會發現,這種方式形成的配置檔案幾乎可以相當於一門指令碼語言了。而且這個指令碼的直譯器也是我們自己寫的,另外關鍵是它是開發的,你可以為它動態地加入一些新的類以增加它的功能。

不要以為這僅僅是一個設想,雖然要開發成一門完備的指令碼語言確實比較麻煩。但是在一些網路端的大型專案中,通過配置檔案 + ClassLoader + 反射機制結合形成的這種軟體解耦和方式已經用得比較普遍了。 所以,在此我不是在提出一種設想,而是在介紹業界處理此類問題的一種解決方案。

反射安全

文章讀到這裡,我想你應該由衷地感嘆,Java反射機制實在是太強大了。但是,如果你有一些安全意識的話,就會發現Java這個機制強大得似乎有些過頭了。前面我們提到,Java反射甚至可以訪問private方法和屬性。為了讓大家對Java反射有更全面的瞭解,樹立正確的人生觀價值觀,本小節將對Java的安全問題做一個概要性的介紹。

相對於C++來說,Java算是比較安全的語言了。這與它們的執行機制有密切的關係,C++執行於本地,也就是說幾乎所有程式的許可權理論上都是相同的。而Java由於是執行於虛擬機器中,而不直接與外部聯絡,所以實際上Java的執行環境是一個“沙盒”環境。 Java的安全機制其實是比較複雜的,至少對於我來說是如此。作為Java的安全模型,它包括了:位元組碼驗證器、類載入器、安全管理器、訪問控制器等一系列的元件。之前文中提到過,我把Android安全許可權劃分為三個等級:第一級是針對編譯期的“@hide”標記;第二級是針對訪問許可權的private等修飾;第三級則是以安全管理器為託管的Permission機制。

Java反射確實可以訪問private的方法和屬性,這是繞過第二級安全機制的方法(之一)。它其實是Java本身為了某種目的而留下的類似於“後門”的東西,或者說是為了方便除錯?不管如何,它的原理其實是關閉訪問安全檢查。

如果你具有獨立鑽研的精神的話,你會發現之前我們提到的Field、Method和Constructor類,它們都有一個共同的父類AccessibleObject 。AccessibleObject 有一個公共方法:void setAccessible(boolean flag)。正是這個方法,讓我們可以改變動態的開啟或者關閉訪問安全檢查,從而訪問到原本是private的方法或域。另外,訪問安全檢查是一件比較耗時的操作,關閉它反射的效能也會有較大提升。

不要認為我們繞過了前兩級安全機制就沾沾自喜了,因為這兩級安全並不是真正為了安全而設定的。它們的作用更多的是為了更好的完善規則。而第三級安全才是真正為了防止惡意攻擊而出現的。在這一級的防護下,你甚至可能都無法完成反射(ReflectPermission),其他的一切自然無從說起。

七、通過反射了解集合泛型的本質

首先下結論:

Java中集合的泛型,是防止錯誤輸入的,只在編譯階段有效,繞過編譯到了執行期就無效了。 下面通過一個例項來驗證:

package com.Soul.reflect;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * 集合泛型的本質
 * @description
 * @author Soul
 * @date 2016年4月2日上午2:54:11
 */
public class Generic {
    public static void main(String[] args) {
        List list1 = new ArrayList(); // 沒有泛型 
        List<String> list2 = new ArrayList<String>(); // 有泛型


        /*
         * 1.首先觀察正常新增元素方式,在編譯器檢查泛型,
         * 這個時候如果list2新增int型別會報錯
         */
        list2.add("hello");
//      list2.add(20); // 報錯!list2有泛型限制,只能新增String,新增int報錯
        System.out.println("list2的長度是:" + list2.size()); // 此時list2長度為1


        /*
         * 2.然後通過反射新增元素方式,在執行期動態載入類,首先得到list1和list2
         * 的類型別相同,然後再通過方法反射繞過編譯器來呼叫add方法,看能否插入int
         * 型的元素
         */
        Class c1 = list1.getClass();
        Class c2 = list2.getClass();
        System.out.println(c1 == c2); // 結果:true,說明類型別完全相同

        // 驗證:我們可以通過方法的反射來給list2新增元素,這樣可以繞過編譯檢查
        try {
            Method m = c2.getMethod("add", Object.class); // 通過方法反射得到add方法
            m.invoke(list2, 20); // 給list2新增一個int型的,上面顯示在編譯器是會報錯的
            System.out.println("list2的長度是:" + list2.size()); // 結果:2,說明list2長度增加了,並沒有泛型檢查
        } catch (Exception e) {
            e.printStackTrace();
        }

        /*
         * 綜上可以看出,在編譯器的時候,泛型會限制集合內元素型別保持一致,但是編譯器結束進入
         * 執行期以後,泛型就不再起作用了,即使是不同型別的元素也可以插入集合。
         */
    }
}
複製程式碼

輸出結果

list2的長度是:1 true list2的長度是:2


#### YunSoul技術分享,掃碼關注微信公眾號##
    ——只要你學會了之前所不會的東西,只要今天的你強過了昨天的你,那你就一直是在進階的路上了。 Java反射機制

相關文章