Java 基礎與提高幹貨系列—Java 反射機制 | 掘金技術徵文

嘟嘟MD發表於2017-05-01

原本地址:Java基礎與提高幹貨系列——Java反射機制
部落格地址:tengj.top/

前言

今天介紹下Java的反射機制,以前我們獲取一個類的例項都是使用new一個例項出來。那樣太low了,今天跟我一起來學習學習一種更加高大上的方式來實現。

正文

Java反射機制定義

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

反射機制的優點與缺點

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

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

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

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

理解Class類和類型別

想要了解反射首先理解一下Class類,它是反射實現的基礎。
類是java.lang.Class類的例項物件,而Class是所有類的類(There is a class named Class)
對於普通的物件,我們一般都會這樣建立和表示:

Code code1 = new Code();複製程式碼

上面說了,所有的類都是Class的物件,那麼如何表示呢,可不可以通過如下方式呢:

Class c = new Class();複製程式碼

但是我們檢視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反射相關操作

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

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

下面來具體介紹

獲取成員方法資訊

單獨獲取某一個方法是通過Class類的以下方法獲得的:

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

兩個引數分別是方法名和方法引數類的類型別列表。
例如類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成員變數,包括其父類變數複製程式碼

引數是成員變數的名字。
例如一個類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構造器,包括父類複製程式碼

這個引數為建構函式引數類的類型別列表。
例如類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)複製程式碼

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

首先下結論:

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

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

/**
 * 集合泛型的本質
 * @description
 * @author Trigl
 * @date 2016年4月2日上午2:54:11
 */
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複製程式碼

總結

到此,Java反射機制入門的差不多了,我是複習SpringMVC裡面IOC/DI的時候,底層原理是通過Java反射來實現的,希望這篇筆記也對你有用。

參考

Java反射機制深入詳解
Java反射入門
Java反射機制
java反射詳解
Java 反射機制淺析
反射機制的理解及其用途

整理的思維導圖

個人整理的Java反射機制的思維導圖,匯出的圖片無法檢視備註的一些資訊,所以需要原始檔的童鞋可以關注我個人主頁上的公眾號,回覆反射機制即可獲取原始檔。

Java 基礎與提高幹貨系列—Java 反射機制 | 掘金技術徵文

一直覺得自己寫的不是技術,而是情懷,一篇篇文章是自己這一路走來的痕跡。靠專業技能的成功是最具可複製性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡,希望未來技術之巔上有你也有我。

訂閱博主微信公眾號:嘟爺java超神學堂(javaLearn)三大好處:

  • 獲取最新博主部落格更新資訊,首發公眾號
  • 獲取大量視訊,電子書,精品破解軟體資源
  • 可以跟博主聊天,歡迎程式媛妹妹來撩我

Java 基礎與提高幹貨系列—Java 反射機制 | 掘金技術徵文

掘金技術徵文第三期:聊聊你的最佳實踐

相關文章