Java反射說得透徹一些

[TOC]html

不少時候咱們會遇到別人問一個問題:你給我講一下反射,究竟是什麼東西?怎麼實現的?咱們能用反射來作什麼?它有什麼優缺點?下面咱們會圍繞着這幾個問題展開:
java

1、反射機制是什麼?

反射是什麼?什麼是反?什麼是正射?
有反就有正,咱們知道正常狀況, 若是咱們但願建立一個對象,會使用如下的語句:數組

Person person = new Person();

其實咱們第一次執行上面的語句的時候,JVM會先加載Person.class,加載到內存完以後,在方法區/堆中會建立了一個Class對象,對應這個Person類。這裏有爭議,有人說是在方法區,有些人說是在堆。我的感受應該JVM規範說是在方法區,可是不是強制要求,並且不一樣版本的JVM實現也不同。具體參考如下連接,這裏不作解釋:
https://www.cnblogs.com/xy-nb...
而上面正常的初始化對象的方法,也能夠說是「正射」,就是使用Class對象建立出一個Person對象。安全

而反射則相反,是根據Person對象,獲取到Class對象,而後能夠獲取到Person類的相關信息,進行初始化或者調用等一系列操做。框架

運行狀態時,能夠構造任何一個類的對象,獲取到任意一個對象所屬的類信息,以及這個類的成員變量或者方法,能夠調用任意一個對象的屬性或者方法。能夠理解爲具有了 動態加載對象 以及 對對象的基本信息進行剖析和使用 的能力。ide

提供的功能包括:函數

  • 1.在運行時判斷一個對象所屬的類
  • 2.在運行時構造任意一個類的對象
  • 3.在運行時獲取一個類定義的成員變量以及方法
  • 4.在運行時調用任意一個對象的方法
  • 5.生成動態代理

靈活,強大,能夠在運行時裝配,無需在組件之間進行源代碼連接,可是使用不當效率會有影響。全部類的對象都是Class的實例。
既然咱們能夠對類的全限定名,方法以及參數等進行配置,完成對象的初始化,那就是至關於增長了java的可配置性。學習

這裏特別須要明確的一點:類自己也是一個對象,方法也是一個對象,在Java裏面萬物皆可對象,除了基礎數據類型...測試

2、反射的具體使用

2.1 獲取對象的包名以及類名

package invocation;
public class MyInvocation {
    public static void main(String[] args) {
        getClassNameTest();
    }
    
    public static void getClassNameTest(){
        MyInvocation myInvocation = new MyInvocation();
        System.out.println("class: " + myInvocation.getClass());
        System.out.println("simpleName: " + myInvocation.getClass().getSimpleName());
        System.out.println("name: " + myInvocation.getClass().getName());
        System.out.println("package: " +
                "" + myInvocation.getClass().getPackage());
    }
}

運行結果:優化

class: class invocation.MyInvocation
simpleName: MyInvocation
name: invocation.MyInvocation
package: package invocation

由上面結果咱們能夠看到:
1.getClass():打印會帶着class+全類名
2.getClass().getSimpleName():只會打印出類名
3.getName():會打印全類名
4.getClass().getPackage():打印出package+包名

getClass()獲取到的是一個對象,getPackage()也是。

2.2 獲取Class對象

在java中,一切皆對象。java中能夠分爲兩種對象,實例對象和Class對象。這裏咱們說的獲取Class對象,其實就是第二種,Class對象表明的是每一個類在運行時的類型信息,指和類相關的信息。好比有一個Student類,咱們用Student student = new Student()new一個對象出來,這個時候Student這個類的信息其實就是存放在一個對象中,這個對象就是Class類的對象,而student這個實例對象也會和Class對象關聯起來。
咱們有三種方式能夠獲取一個類在運行時的Class對象,分別是

  • Class.forName("com.Student")
  • student.getClass()
  • Student.class

實例代碼以下:

package invocation;

public class MyInvocation {
    public static void main(String[] args) {
        getClassTest();
    }
    public static void getClassTest(){
        Class<?> invocation1 = null;
        Class<?> invocation2 = null;
        Class<?> invocation3 = null;
        try {
            // 最經常使用的方法
            invocation1 = Class.forName("invocation.MyInvocation");
        }catch (Exception ex){
            ex.printStackTrace();
        }
        invocation2 = new MyInvocation().getClass();
        invocation3 = MyInvocation.class;
        System.out.println(invocation1);
        System.out.println(invocation2);
        System.out.println(invocation3);
    }
}

執行的結果以下,三個結果同樣:

class invocation.MyInvocation
class invocation.MyInvocation
class invocation.MyInvocation

2.3 getInstance()獲取指定類型的實例化對象

首先咱們有一個Student類,後面都會沿用這個類,將再也不重複。

class Student{
    private int age;

    private String name;

    public Student() {
    }
    public Student(int age) {
        this.age = age;
    }

    public Student(String name) {
        this.name = name;
    }
    
    public Student(int age, String name) {
        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 "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

咱們可使用getInstance()方法構造出一個Student的對象:

public static void getInstanceTest() {
        try {
            Class<?> stduentInvocation = Class.forName("invocation.Student");
            Student student = (Student) stduentInvocation.newInstance();
            student.setAge(9);
            student.setName("Hahs");
            System.out.println(student);

        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
    
    
輸出結果以下:
Student{age=9, name='Hahs'}

可是若是咱們取消不寫Student的無參構造方法呢?就會出現下面的報錯:

java.lang.InstantiationException: invocation.Student
    at java.lang.Class.newInstance(Class.java:427)
    at invocation.MyInvocation.getInstanceTest(MyInvocation.java:40)
    at invocation.MyInvocation.main(MyInvocation.java:8)
Caused by: java.lang.NoSuchMethodException: invocation.Student.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 2 more

這是由於咱們重寫了構造方法,並且是有參構造方法,若是不寫構造方法,那麼每一個類都會默認有無參構造方法,重寫了就不會有無參構造方法了,因此咱們調用newInstance()的時候,會報沒有這個方法的錯誤。值得注意的是,newInstance()是一個無參構造方法。

2.4 經過構造函數對象實例化對象

除了newInstance()方法以外,其實咱們還能夠經過構造函數對象獲取實例化對象,怎麼理解?這裏只構造函數對象,而不是構造函數,也就是構造函數其實就是一個對象,咱們先獲取構造函數對象,固然也可使用來實例化對象。

能夠先獲取一個類的全部的構造方法,而後遍歷輸出:

public static void testConstruct(){
        try {
            Class<?> stduentInvocation = Class.forName("invocation.Student");
            Constructor<?> cons[] = stduentInvocation.getConstructors();
            for(int i=0;i<cons.length;i++){
                System.out.println(cons[i]);
            }

        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

輸出結果:

public invocation.Student(int,java.lang.String)
public invocation.Student(java.lang.String)
public invocation.Student(int)
public invocation.Student()

取出一個構造函數咱們能夠獲取到它的各類信息,包括參數,參數個數,類型等等:

public static void constructGetInstance() {
        try {
            Class<?> stduentInvocation = Class.forName("invocation.Student");
            Constructor<?> cons[] = stduentInvocation.getConstructors();
            Constructor constructors = cons[0];
            System.out.println("name: " + constructors.getName());
            System.out.println("modifier: " + constructors.getModifiers());
            System.out.println("parameterCount: " + constructors.getParameterCount());
            System.out.println("構造參數類型以下:");
            for (int i = 0; i < constructors.getParameterTypes().length; i++) {
                System.out.println(constructors.getParameterTypes()[i].getName());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

輸出結果,modifier是權限修飾符,1表示爲public,咱們能夠知道獲取到的構造函數是兩個參數的,第一個是int,第二個是String類型,看來獲取出來的順序並不必定是咱們書寫代碼的順序。

name: invocation.Student
modifier: 1
parameterCount: 2
構造參數類型以下:
int
java.lang.String

既然咱們能夠獲取到構造方法這個對象了,那麼咱們可不能夠經過它去構造一個對象呢?答案確定是能夠!!!
下面咱們用不一樣的構造函數來建立對象:

public static void constructGetInstanceTest() {
        try {
            Class<?> stduentInvocation = Class.forName("invocation.Student");
            Constructor<?> cons[] = stduentInvocation.getConstructors();
            // 一共定義了4個構造器
            Student student1 = (Student) cons[0].newInstance(9,"Sam");
            Student student2 = (Student) cons[1].newInstance("Sam");
            Student student3 = (Student) cons[2].newInstance(9);
            Student student4 = (Student) cons[3].newInstance();
            System.out.println(student1);
            System.out.println(student2);
            System.out.println(student3);
            System.out.println(student4);

        } catch (Exception ex) {
            ex.printStackTrace();
        }

輸出以下:

Student{age=9, name='Sam'}
Student{age=0, name='Sam'}
Student{age=9, name='null'}
Student{age=0, name='null'}

構造器的順序咱們是必須一一針對的,要不會報一下的參數不匹配的錯誤:

java.lang.IllegalArgumentException: argument type mismatch
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at invocation.MyInvocation.constructGetInstanceTest(MyInvocation.java:85)
    at invocation.MyInvocation.main(MyInvocation.java:8)

2.5 獲取類繼承的接口

經過反射咱們能夠獲取接口的方法,若是咱們知道某個類實現了接口的方法,一樣能夠作到經過類名建立對象調用到接口的方法。

首先咱們定義兩個接口,一個InSchool:

public interface InSchool {
    public void attendClasses();
}

一個AtHome:

public interface AtHome {
    public void doHomeWork();
}

建立一個實現兩個接口的類Student.java

public class Student implements AtHome, InSchool {
    public void doHomeWork() {
        System.out.println("I am a student,I am doing homework at home");
    }

    public void attendClasses() {
        System.out.println("I am a student,I am attend class in school");
    }
}

測試代碼以下:

public class Test {
    public static void main(String[] args) throws Exception {
        Class<?> studentClass = Class.forName("invocation.Student");
        Class<?>[] interfaces = studentClass.getInterfaces();
        for (Class c : interfaces) {
            // 獲取接口
            System.out.println(c);
            // 獲取接口裏面的方法
            Method[] methods = c.getMethods();
            // 遍歷接口的方法
            for (Method method : methods) {
                // 經過反射建立對象
                Student student = (Student) studentClass.newInstance();
                // 經過反射調用方法
                method.invoke(student, null);
            }
        }
    }
}

結果以下:

能夠看出其實咱們能夠獲取到接口的數組,而且裏面的順序是咱們繼承的順序,經過接口的Class對象,咱們能夠獲取到接口的方法,而後經過方法反射調用實現類的方法,由於這是一個無參數的方法,因此只須要傳null便可。

2.6 獲取父類相關信息

主要是使用getSuperclass()方法獲取父類,固然也能夠獲取父類的方法,執行父類的方法,首先建立一個Animal.java:

public class Animal {
    public void doSomething(){
        System.out.println("animal do something");
    }
}

Dog.java繼承於Animal.java

public class Dog extends Animal{
    public void doSomething(){
        System.out.println("Dog do something");
    }
}

咱們能夠經過反射建立Dog對象,獲取其父類Animal以及建立對象,固然也能夠獲取Animal的默認父類Object

public class Test {
    public static void main(String[] args) throws Exception {
        Class<?> dogClass = Class.forName("invocation02.Dog");
        System.out.println(dogClass);
        invoke(dogClass);

        Class<?> animalClass = dogClass.getSuperclass();
        System.out.println(animalClass);
        invoke(animalClass);

        Class<?> objectClass = animalClass.getSuperclass();
        System.out.println(objectClass);
        invoke(objectClass);
    }

    public static void invoke(Class<?> myClass) throws Exception {
        Method[] methods = myClass.getMethods();
        // 遍歷接口的方法
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase("doSomething")) {
                // 經過反射調用方法
                method.invoke(myClass.newInstance(), null);
            }
        }
    }
}

輸入以下:

2.7 獲取當前類的公有屬性和私有屬性以及更新

建立一個Person.java,裏面有靜態變量,非靜態變量,以及publicprotected,private不一樣修飾的屬性。

public class Person {

    public static String type ;

    private static String subType ;

    // 名字(公開)
    public String name;

    protected String gender;

    private String address;

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

使用getFields()能夠獲取到public的屬性,包括static屬性,使用getDeclaredFields()能夠獲取全部聲明的屬性,不論是publicprotected,private不一樣修飾的屬性。

修改public屬性,只須要field.set(object,value)便可,可是private屬性不能直接set,不然會報如下的錯誤。

Exception in thread "main" java.lang.IllegalAccessException: Class invocation03.Tests can not access a member of class invocation03.Person with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
    at java.lang.reflect.Field.set(Field.java:761)
    at invocation03.Tests.main(Tests.java:21)

那麼須要怎麼作呢?private默認是不容許外界操做其值的,這裏咱們可使用field.setAccessible(true);,至關於打開了操做的權限。

那static的屬性修改和非static的同樣,可是咱們怎麼獲取呢?
若是是public修飾的,能夠直接用類名獲取到,若是是private修飾的,那麼須要使用filed.get(object),這個方法其實對上面說的全部的屬性均可以的。
測試代碼以下

public class Tests {
    public static void main(String[] args) throws Exception{
        Class<?> personClass = Class.forName("invocation03.Person");
        Field[] fields = personClass.getFields();
        // 獲取公開的屬性
        for(Field field:fields){
            System.out.println(field);
        }
        System.out.println("=================");
        // 獲取全部聲明的屬性
        Field[] declaredFields = personClass.getDeclaredFields();
        for(Field field:declaredFields){
            System.out.println(field);
        }
        System.out.println("=================");
        Person person = (Person) personClass.newInstance();
        person.name = "Sam";
        System.out.println(person);

        // 修改public屬性
        Field fieldName = personClass.getDeclaredField("name");
        fieldName.set(person,"Jone");

        // 修改private屬性
        Field addressName = personClass.getDeclaredField("address");
        // 須要修改權限
        addressName.setAccessible(true);
        addressName.set(person,"東風路47號");
        System.out.println(person);

        // 修改static 靜態public屬性
        Field typeName = personClass.getDeclaredField("type");
        typeName.set(person,"人類");
        System.out.println(Person.type);

        // 修改靜態 private屬性
        Field subType = personClass.getDeclaredField("subType");
        subType.setAccessible(true);
        subType.set(person,"黃種人");
        System.out.println(subType.get(person));
    }
}

結果:

從結果能夠看出,不論是public,仍是protectedprivate修飾的,咱們均可以經過反射對其進行查詢和修改,不論是靜態變量仍是非靜態變量。
getDeclaredField()能夠獲取到全部聲明的屬性,而getFields()則只能獲取到public的屬性。對於非public的屬性,咱們須要修改其權限才能訪問和修改:field.setAccessible(true)

獲取屬性值須要使用field.get(object),值得注意的是:每一個屬性,其自己就是對象

2.8 獲取以及調用類的公有/私有方法

既然能夠獲取到公有屬性和私有屬性,那麼我想,執行公有方法和私有方法應該都不是什麼問題?

那下面咱們一塊兒來學習一下...

先定義一個類,包含各類修飾符,以及是否包含參數,是否爲靜態方法,Person.java:

public class Person {
    // 非靜態公有無參數
    public void read(){
        System.out.println("reading...");
    }

    // 非靜態公有無參數有返回
    public String getName(){
        return "Sam";
    }

    // 非靜態公有帶參數   
    public int readABookPercent(String name){
        System.out.println("read "+name);
        return 80;
    }

    // 私有有返回值
    private String getAddress(){
        return "東方路";
    }

    // 公有靜態無參數無返回值
    public static void staticMethod(){
        System.out.println("static public method");
    }

    // 公有靜態有參數
    public static void staticMethodWithArgs(String args){
        System.out.println("static public method:"+args);
    }

    // 私有靜態方法
    private static void staticPrivateMethod(){
        System.out.println("static private method");
    }
}

首先咱們來看看獲取裏面全部的方法:

public class Tests {
    public static void main(String[] args) throws Exception {
        Class<?> personClass = Class.forName("invocation03.Person");
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }

        System.out.println("=============================================");
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            System.out.println(method);
        }
    }
}

結果以下:

咦,咱們發現getMethods()確實能夠獲取全部的公有的方法,可是有一個問題,就是他會把父類的也獲取到,也就是上面圖片綠色框裏面的,咱們知道全部的類默認都繼承了Object類,因此它把Object的那些方法都獲取到了。
getDeclaredMethods確實能夠獲取到公有和私有的方法,不論是靜態仍是非靜態,可是它是獲取不到父類的方法的。

那若是咱們想調用方法呢?先試試調用非靜態方法:

public class Tests {
    public static void main(String[] args) throws Exception {
        Class<?> personClass = Class.forName("invocation03.Person");
        Person person = (Person) personClass.newInstance();
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("read")){
                method.invoke(person,null);
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("getName")){
                System.out.println(method.invoke(person,null));
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("readABookPercent")){
                System.out.println(method.invoke(person,"Sam"));
                System.out.println("===================");
            }
        }

    }
}

結果以下,能夠看出method.invoke(person,null);是調用無參數的方法,而method.invoke(person,"Sam")則是調用有參數的方法,要是有更多參數,也只須要在裏面多加一個參數便可,返回值也一樣能夠獲取到。

那麼private方法呢?咱們照着來試試,試試就試試,who 怕 who?

public class Tests {
    public static void main(String[] args) throws Exception {
        Class<?> personClass = Class.forName("invocation03.Person");
        Person person = (Person) personClass.newInstance();
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("getAddress")){
                method.invoke(person,null);
            }
        }

    }
}

結果報錯了:

Exception in thread "main" java.lang.IllegalAccessException: Class invocation03.Tests can not access a member of class invocation03.Person with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
    at java.lang.reflect.Method.invoke(Method.java:491)
    at invocation03.Tests.main(Tests.java:13)

一看就是沒有權限,小場面,不要慌,我來操做一波,只要加上

method.setAccessible(true);

哦豁,完美解決了...

那麼問題來了,上面說的都是非靜態的,我就想要調用靜態的方法。
固然用上面的方法,對象也能夠直接調用到類的方法的:

一點問題都沒有,爲何輸出結果有幾個null,那是由於這函數是無返回值的呀,笨蛋...

若是我不想用遍歷方法的方式,再去判斷怎麼辦?能不能直接獲取到我想要的方法啊?那答案確定是能夠啊。

public class Tests {
    public static void main(String[] args) throws Exception {
        Class<?> personClass = Class.forName("invocation03.Person");
        Person person = (Person) personClass.newInstance();
        Method method = personClass.getMethod("readABookPercent", String.class);
        method.invoke(person, "唐詩三百首");
    }
}

結果和上面調用的徹底同樣,圖我就不放了,就一行字。要是這個方法沒有參數呢?那就給一個null就能夠啦。或者不給也能夠。

public class Tests {
    public static void main(String[] args) throws Exception {
        Class<?> personClass = Class.forName("invocation03.Person");
        Person person = (Person) personClass.newInstance();
        Method method = personClass.getMethod("getName",null);
        System.out.println(method.invoke(person));
    }
}

3、反射的優缺點

3.1 優勢

反射能夠在不知道會運行哪個類的狀況下,獲取到類的信息,建立對象以及操做對象。這其實很方便於拓展,因此反射會是框架設計的靈魂,由於框架在設計的時候,爲了下降耦合度,確定是須要考慮拓展等功能的,不能將類型寫死,硬編碼。

下降耦合度,變得很靈活,在運行時去肯定類型,綁定對象,體現了多態功能。

3.2 缺點

這麼好用,沒有缺點?怎麼可能!!!有利就有弊,事物都是有雙面性的。
即便功能很強大,可是反射是須要動態類型的,JVM沒有辦法優化這部分代碼,執行效率相對直接初始化對象較低。通常業務代碼不建議使用。

反射能夠修改權限,好比上面訪問到private這些方法和屬性,這是會破壞封裝性的,有安全隱患,有時候,還會破壞單例的設計。

反射會使代碼變得複雜,不容易維護,畢竟代碼仍是要先寫給人看的嘛,逃~

此文章僅表明本身(本菜鳥)學習積累記錄,或者學習筆記,若有侵權,請聯繫做者刪除。人無完人,文章也同樣,文筆稚嫩,在下不才,勿噴,若是有錯誤之處,還望指出,感激涕零~

技術之路不在一時,山高水長,縱使緩慢,馳而不息。

公衆號:「秦懷雜貨店」

相關文章
相關標籤/搜索