JAVA特性 之 反射(Reflection)

二一點發表於2018-04-24

什麼是反射?

反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。

用一句話總結就是反射可以實現在執行時可以知道任意一個類的屬性和方法。

反射能做什麼?

  • 反射機制主要提供了以下功能:
  • 在執行時判斷任意一個物件所屬的類;
  • 在執行時構造任意一個類的物件;
  • 在執行時判斷任意一個類所具有的成員變數和方法;
  • 在執行時呼叫任意一個物件的方法;
  • 生成動態代理。

Java 反射機制的應用場景

  • 逆向程式碼 ,例如反編譯
  • 與註解相結合的框架 例如Retrofit
  • 單純的反射機制應用框架 例如EventBus
  • 動態生成類框架 例如Gson

反射機制的優點與缺點

為什麼要用反射機制?直接建立物件不就可以了嗎,這就涉及到了動態與靜態的概念。

靜態編譯:在編譯時確定型別,繫結物件,即通過。

動態編譯:執行時確定型別,繫結物件。動態編譯最大限度發揮了java的靈活性,體現了多型的應用,有以降低類之間的藕合性。

優點

可以實現動態建立物件和編譯,體現出很大的靈活性,特別是在J2EE的開發中它的靈活性就表現的十分明顯。比如,一個大型的軟體,不可能一次就把把它設計的很完美,當這個程式編譯後,釋出了,當發現需要更新某些功能時,我們不可能要使用者把以前的解除安裝,再重新安裝新的版本,假如這樣的話,這個軟體肯定是沒有多少人用的。採用靜態的話,需要把整個程式重新編譯一次才可以實現功能的更新,而採用反射機制的話,它就可以不用解除安裝,只需要在執行時才動態的建立和編譯,就可以實現該功能。

缺點

對效能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於只直接執行相同的操作。

理解Class類和類型別

想要了解反射首先理解一下Class類,它是反射實現的基礎。

類是java.lang.Class類的例項物件,而Class是所有類的類(There is a class named Class)

對於普通的物件,我們一般都會這樣建立和表示:

Code code1 = new Code();

但是我們檢視Class的原始碼時,是這樣寫的:

private  Class(ClassLoader loader) { 
    classLoader = loader; 
}

可以看到構造器是私有的,只有JVM可以建立Class的物件,因此不可以像普通類一樣new一個Class物件,雖然我們不能new一個Class物件,但是卻可以通過已有的類得到一個Class物件,共有三種方式,如下:

Class c1 = Code.class; 這說明任何一個類都有一個隱含的靜態成員變數class,這種方式是通過獲取類的靜態成員變數class得到的
Class c2 = code1.getClass(); code1是Code的一個物件,這種方式是通過一個類的物件的getClass()方法獲得的 
Class c3 = Class.forName("com.trigl.reflect.Code"); 這種方法是Class類呼叫forName方法,通過一個類的全量限定名獲得

這裡,c1、c2、c3都是Class的物件,他們是完全一樣的,而且有個學名,叫做Code的類型別(class type)。

這裡就讓人奇怪了,前面不是說Code是Class的物件嗎,而c1、c2、c3也是Class的物件,那麼Code和c1、c2、c3不就一樣了嗎?為什麼還叫Code什麼類型別?這裡不要糾結於它們是否相同,只要理解類型別是幹什麼的就好了,顧名思義,類型別就是類的型別,也就是描述一個類是什麼,都有哪些東西,所以我們可以通過類型別知道一個類的屬性和方法,並且可以呼叫一個類的屬性和方法,這就是反射的基礎。

舉個簡單例子程式碼:

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        //第一種:Class c1 = Code.class;
        Class class1=ReflectDemo.class;
        System.out.println(class1.getName());

        //第二種:Class c2 = code1.getClass();
        ReflectDemo demo2= new ReflectDemo();
        Class c2 = demo2.getClass();
        System.out.println(c2.getName());

        //第三種:Class c3 = Class.forName("com.trigl.reflect.Code");
        Class class3 = Class.forName("com.tengj.reflect.ReflectDemo");
        System.out.println(class3.getName());
    }
}

執行結果:

com.tengj.reflect.ReflectDemo
com.tengj.reflect.ReflectDemo
com.tengj.reflect.ReflectDemo

Java反射相關操作

在這裡先看一下sun為我們提供了那些反射機制中的類:

java.lang.Class;
java.lang.reflect.Constructor; java.lang.reflect.Field;
java.lang.reflect.Method;
java.lang.reflect.Modifier;

前面我們知道了怎麼獲取Class,那麼我們可以通過這個Class幹什麼呢?總結如下:

  • 獲取成員方法Method
  • 獲取成員變數Field
  • 獲取建構函式Constructor

一、獲取成員方法資訊

兩個引數分別是方法名和方法引數類的類型別列表。

public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到該類所有的方法,不包括父類的 
public Method getMethod(String name, Class<?>... parameterTypes) // 得到該類所有的public方法,包括父類的

//具體使用
Method[] methods = class1.getDeclaredMethods();//獲取class物件的所有宣告方法 
Method[] allMethods = class1.getMethods();//獲取class物件的所有public方法 包括父類的方法 
Method method = class1.getMethod("info", String.class);//返回次Class物件對應類的、帶指定形參列表的public方法 
Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回次Class物件對應類的、帶指定形參列表的方法

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

public void fun(String name,int age) {
    System.out.println("我叫"+name+",今年"+age+"歲");
}

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

Class c = Class.forName("com.tengj.reflect.Person");  //先生成class
Object o = c.newInstance();                           //newInstance可以初始化一個例項
Method method = c.getMethod("fun", String.class, int.class);//獲取方法
method.invoke(o, "tengj", 10);                         //通過invoke呼叫該方法,引數第一個為例項物件,後面為具體引數值

完整程式碼如下:

public class Person {
    private String name;
    private int age;
    private String msg="hello wrold";
 public String getName() {
        return name;
  }

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

    public int getAge() {
        return age;
  }

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

    public Person() {
    }

    private Person(String name) {
        this.name = name;
  System.out.println(name);
  }

    public void fun() {
        System.out.println("fun");
  }

    public void fun(String name,int age) {
        System.out.println("我叫"+name+",今年"+age+"歲");
  }
}

public class ReflectDemo {
    public static void main(String[] args){
        try {
            Class c = Class.forName("com.tengj.reflect.Person");
            Object o = c.newInstance();
            Method method = c.getMethod("fun", String.class, int.class);
            method.invoke(o, "tengj", 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

我叫tengj,今年10歲

怎樣,是不是感覺很厲害,我們只要知道這個類的路徑全稱就能玩弄它於鼓掌之間。有時候我們想獲取類中所有成員方法的資訊,要怎麼辦。可以通過以下幾步來實現:

1、獲取所有方法的陣列:

Class c = Class.forName("com.tengj.reflect.Person");
Method[] methods = c.getDeclaredMethods(); // 得到該類所有的方法,不包括父類的
或者:
Method[] methods = c.getMethods();// 得到該類所有的public方法,包括父類的

2、然後迴圈這個陣列就得到每個方法了:

for (Method method : methods)

完整程式碼如下:person類跟上面一樣,這裡以及後面就不貼出來了,只貼關鍵程式碼:

public class ReflectDemo {
    public static void main(String[] args){
        try {
            Class c = Class.forName("com.tengj.reflect.Person");
            Method[] methods = c.getDeclaredMethods();
            for(Method m:methods){
                String  methodName= m.getName();
                System.out.println(methodName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

getName
setName
setAge
fun
fun
getAge

這裡如果把c.getDeclaredMethods();改成c.getMethods();執行結果如下,多了很多方法,以為把Object裡面的方法也列印出來了,因為Object是所有類的父類:

getName
setName
getAge
setAge
fun
fun
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll

二、獲取成員變數資訊

想一想成員變數中都包括什麼:成員變數型別+成員變數名;類的成員變數也是一個物件,它是java.lang.reflect.Field的一個物件,所以我們通過java.lang.reflect.Field裡面封裝的方法來獲取這些資訊。

單獨獲取某個成員變數,通過Class類的以下方法實現,引數是成員變數的名字:

public Field getDeclaredField(String name) // 獲得該類自身宣告的所有變數,不包括其父類的變數
public Field getField(String name) // 獲得該類自所有的public成員變數,包括其父類變數

//具體實現
Field[] allFields = class1.getDeclaredFields();//獲取class物件的所有屬性 
Field[] publicFields = class1.getFields();//獲取class物件的public屬性 
Field ageField = class1.getDeclaredField("age");//獲取class指定屬性 
Field desField = class1.getField("des");//獲取class指定的public屬性

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

private int n;

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

Class c = a.getClass();
Field field = c.getDeclaredField("n");

完整程式碼如下:

public class ReflectDemo {
    public static void main(String[] args){
        try {
            Class c = Class.forName("com.tengj.reflect.Person");
            //獲取成員變數
            Field field = c.getDeclaredField("msg"); //因為msg變數是private的,所以不能用getField方法
            Object o = c.newInstance();
            field.setAccessible(true);//設定是否允許訪問,因為該變數是private的,所以要手動設定允許訪問,如果msg是public的就不需要這行了。
            Object msg = field.get(o);
            System.out.println(msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

hello wrold

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

1、取所有成員變數的陣列:

Field[] fields = c.getDeclaredFields();

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

for (Field field : fields)

完整程式碼:

public class ReflectDemo {
    public static void main(String[] args){
        try {
            Class c = Class.forName("com.tengj.reflect.Person");
            Field[] fields = c.getDeclaredFields();
            for(Field field :fields){
                System.out.println(field.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

name
age
msg

三、獲取建構函式

最後再想一想建構函式中都包括什麼:建構函式引數同上,類的成建構函式也是一個物件,它是java.lang.reflect.Constructor的一個物件,所以我們通過java.lang.reflect.Constructor裡面封裝的方法來獲取這些資訊。

單獨獲取某個建構函式,通過Class類的以下方法實現,這個引數為建構函式引數類的類型別列表:

public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) //  獲得該類所有的構造器,不包括其父類的構造器
public Constructor<T> getConstructor(Class<?>... parameterTypes) // 獲得該類所以public構造器,包括父類

//具體
Constructor<?>[] allConstructors = class1.getDeclaredConstructors();//獲取class物件的所有宣告建構函式 
Constructor<?>[] publicConstructors = class1.getConstructors();//獲取class物件public建構函式 
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//獲取指定宣告建構函式 
Constructor publicConstructor = class1.getConstructor(String.class);//獲取指定宣告的public建構函式

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

public A(String a, int b) {
    // code body
}

那麼就可以通過:

Constructor constructor = a.getDeclaredConstructor(String.class, int.class);

來獲取這個建構函式。完整程式碼:

public class ReflectDemo {
    public static void main(String[] args){
        try {
            Class c = Class.forName("com.tengj.reflect.Person");
            //獲取建構函式
            Constructor constructor = c.getDeclaredConstructor(String.class);
            constructor.setAccessible(true);//設定是否允許訪問,因為該構造器是private的,所以要手動設定允許訪問,如果構造器是public的就不需要這行了。
            constructor.newInstance("tengj");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

tengj

注意:Class的newInstance方法,只能建立只包含無引數的建構函式的類,如果某類只有帶引數的建構函式,那麼就要使用另外一種方式:

fromClass.getDeclaredConstructor(String.class).newInstance("tengj");

獲取所有的建構函式,可以通過以下步驟實現:

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

Constructor[] constructors = c.getDeclaredConstructors();

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

for (Constructor constructor : constructors)

完整程式碼:

public class ReflectDemo {
    public static void main(String[] args){
            Constructor[] constructors = c.getDeclaredConstructors();
            for(Constructor constructor:constructors){
                System.out.println(constructor);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

public com.tengj.reflect.Person()
public com.tengj.reflect.Person(java.lang.String)

四、其他方法

註解需要用到的:

Annotation[] annotations = (Annotation[]) class1.getAnnotations();//獲取class物件的所有註解 
Annotation annotation = (Annotation) class1.getAnnotation(Deprecated.class);//獲取class物件指定註解 
Type genericSuperclass = class1.getGenericSuperclass();//獲取class物件的直接超類的 
Type Type[] interfaceTypes = class1.getGenericInterfaces();//獲取class物件的所有介面的type集合

獲取class物件的資訊:

boolean isPrimitive = class1.isPrimitive();//判斷是否是基礎型別 
boolean isArray = class1.isArray();//判斷是否是集合類
boolean isAnnotation = class1.isAnnotation();//判斷是否是註解類 
boolean isInterface = class1.isInterface();//判斷是否是介面類 
boolean isEnum = class1.isEnum();//判斷是否是列舉類 
boolean isAnonymousClass = class1.isAnonymousClass();//判斷是否是匿名內部類 
boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);//判斷是否被某個註解類修飾 
String className = class1.getName();//獲取class名字 包含包名路徑 
Package aPackage = class1.getPackage();//獲取class的包資訊 
String simpleName = class1.getSimpleName();//獲取class類名 
int modifiers = class1.getModifiers();//獲取class訪問許可權 
Class<?>[] declaredClasses = class1.getDeclaredClasses();//內部類 
Class<?> declaringClass = class1.getDeclaringClass();//外部類

getSuperclass():獲取某類的父類  
getInterfaces():獲取某類實現的介面

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

擴充套件的知識點,Java中集合的泛型,是防止錯誤輸入的,只在編譯階段有效,繞過編譯到了執行期就無效了。

下面通過一個例項來驗證:

/**
 * 集合泛型的本質
 */
public class GenericEssence {
    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

六、思維導圖


反射機制是框架技術的原理和核心部分。通過反射機制我們可以動態的通過改變配置檔案(以後是XML檔案)的方式來載入類、呼叫類方法,以及使用類屬性。這樣的話,對於編碼和維護帶來相當大的便利。在程式進行改動的時候,也只會改動相應的功能就行了,呼叫的方法是不用改的。更不會一改就改全身。

相關文章