java反射全解

引言

java中建立對象有幾種方式?

1.使用new關鍵字

2.使用clone方法

3.使用反序列化

4.使用反射

5.使用Unsafe

關於這幾種建立對象方式的詳解,請看這篇文章 java建立對象的五種方式java

接下來主要詳細介紹反射相關知識shell

反射簡介

反射之中包含了一個「反」字,因此想要解釋反射就必須先從「正」開始解釋。
通常狀況下,咱們使用某個類時一定知道它是什麼類,是用來作什麼的。因而咱們直接對這個類進行實例化,以後使用這個類對象進行操做。數據庫

Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);
複製代碼

上面這樣子進行類對象的初始化,咱們能夠理解爲「正」。
而反射則是一開始並不知道我要初始化的類對象是什麼,天然也沒法使用 new 關鍵字來建立對象了。
這時候,咱們使用 JDK 提供的反射 API 進行反射調用:編程

Class clz = Class.forName("com.eft.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);
複製代碼

上面兩段代碼的執行結果,實際上是徹底同樣的。可是其思路徹底不同,第一段代碼在未運行時(編譯時)就已經肯定了要運行的類(Apple),而第二段代碼則是在運行時經過字符串值才得知要運行的類(com.eft.reflect.Apple)。api

因此說什麼是反射?數組

反射就是在運行時才知道要操做的類是什麼,而且能夠在運行時獲取類的完整構造,並調用對應的方法。緩存

官方定義

JAVA反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。安全

反射機制很重要的一點就是「運行時」,其使得咱們能夠在程序運行時加載、探索以及使用編譯期間徹底未知的 .class 文件。換句話說,Java 程序能夠加載一個運行時才得知名稱的 .class 文件,而後獲悉其完整構造,並生成其對象實體、或對其 fields(變量)設值、或調用其 methods(方法)。bash

通俗歸納

反射就是讓你能夠經過名稱來獲得對象的信息(如類,屬性,方法等) 的技術數據結構

核心

Java反射機制是Java語言被視爲「準動態」語言的關鍵性質。Java反射機制的核心就是容許在運行時經過Java Reflection APIs來取得已知名字的class類的內部信息(包括其modifiers(諸如public, static等等)、superclass(例如Object)、實現interfaces(例如Serializable),也包括fields和methods的全部信息),動態地生成此類,並調用其方法或修改其域(甚至是自己聲明爲private的域或方法)

功能

  • 在運行時判斷任意一個對象所屬的類;
  • 在運行時構造任意一個類的對象;
  • 在運行時判斷任意一個類所具備的成員變量和方法;
  • 在運行時調用任意一個對象的方法;
  • 生成動態代理

反射原理

類加載的流程:


類加載的完整過程以下:
(1)在編譯時,Java 編譯器編譯好 .java 文件以後,在磁盤中產生 .class 文件。.class 文件是二進制文件,內容是隻有 JVM 可以識別的機器碼。
(2)JVM 中的類加載器讀取字節碼文件,取出二進制數據,加載到內存中,解析.class 文件內的信息。類加載器會根據類的全限定名來獲取此類的二進制字節流;而後,將字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;接着,在內存中生成表明這個類的 java.lang.Class 對象。
(3)加載結束後,JVM 開始進行鏈接階段(包含驗證、準備、解析)。通過這一系列操做,類的變量會被初始化。

要想使用反射,首先須要得到待操做的類所對應的 Class 對象。Java 中,不管生成某個類的多少個對象,這些對象都會對應於同一個 Class 對象。這個 Class 對象是由 JVM 生成的,經過它可以獲悉整個類的結構。因此,java.lang.Class 能夠視爲全部反射 API 的入口點。
反射的本質就是:在運行時,把 Java 類中的各類成分映射成一個個的 Java 對象。

簡單例子

經過前面引言-使用反射建立對象的例子,咱們瞭解了使用反射建立一個對象的步驟:

獲取類的 Class 對象實例

Class clz = Class.forName("com.eft.reflect.Person");
複製代碼

根據 Class 對象實例獲取 Constructor 對象

Constructor constructor = clz.getConstructor();
複製代碼

使用 Constructor 對象的 newInstance 方法獲取反射類對象

Object personObj = constructor.newInstance();
複製代碼

而若是要調用某一個方法,則須要通過下面的步驟:

  • 獲取方法的 Method 對象
Method setNameMethod = clz.getMethod("setName", String.class);
複製代碼
  • 利用 invoke 方法調用方法
setNameMethod.invoke(personObj, "酸辣湯");
複製代碼

經過反射調用方法的測試代碼:

Class clz = Person.class;
Method setNameMethod = clz.getMethod("setName", String.class);
// Person person= (Person) clz.getConstructor().newInstance();
Person person= (Person) clz.newInstance();
setNameMethod.invoke(person, "酸辣湯888");//調用setName方法,傳入參數

Method getNameMethod = clz.getMethod("getName", null);
String name= (String) getNameMethod.invoke(person,null);//調用getName方法,獲取返回值

System.out.println("name:" +name);

運行結果:
name:酸辣湯888
複製代碼

到這裏,咱們已經可以掌握反射的基本使用。但若是要進一步掌握反射,還須要對反射的經常使用 API 有更深刻的理解

反射API詳解

在 JDK 中,反射相關的 API 能夠分爲下面3個方面:

1、獲取反射的 Class 對象

每一種類型(如:String,Integer,Person...)都會在初次使用時被加載進虛擬機內存的『方法區』中,包含類中定義的屬性字段,方法字節碼等信息。Java 中使用類 java.lang.Class 來指向一個類型信息,經過這個 Class 對象,咱們就能夠獲得該類的全部內部信息

Class沒有公共構造方法。Class對象是在加載類時由Java虛擬機以及經過調用類加載器中的defineClass方法自動構造的。

獲取一個 Class 對象的方法主要有如下三種:

使用類字面常量或TYPE字段

  • 類.class,如Person.class
    • 類字面常量不只能夠應用於普通的類,也能夠應用於接口、數組以及基本數據類型
    • 這種方式不只更簡單,並且更安全,由於它在編譯時就會受到檢查,而且根除了對forName方法的調用,因此也更高效,建議使用「.class」的形式
  • Boolean.TYPE,如Integer.TYPE
    • TYPE是基本數據類型的包裝類型的一個標準字段,它是一個引用,指向對應的基本數據類型的Class對象

表格兩邊等價:

boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

這種方式最直接,但僅能獲取到我已知的類的Class對象,也就是工程內用過的類的對象均可以經過類.class方式獲取其Class對象,可是這種方式有一個不足就是對於未知的類,或者說不可見的類是不能獲取到其Class對象的。

對象.getClass()

如:person.getClass() Java中的祖先類 Object提供了一個方法getClass() 來獲取當着實例的Class對象,這種方式是開發中用的最多的方式,一樣,它也不能獲取到未知的類,好比說某個接口的實現類的Class對象。

API:

public final native Class<?> getClass();
複製代碼

這是一個native方法(一個Native Method就是一個java調用非java代碼的接口),而且不容許子類重寫,因此理論上全部類型的實例都具備同一個 getClass 方法。

使用:

Integer integer = new Integer(12);
Class clz=integer.getClass();
複製代碼

Class.forName("類全路徑")

如:Class.forName("com.eft.xx.Person")
這種方式最經常使用,能夠獲取到任何類的Class對象,前提是該類存在,不然會拋出ClassNotFoundException異常。經過這種方式,咱們只須要知道類的全路徑(徹底限定名)便可獲取到其Class對象(若是存在的話).

API:

//因爲方法區 Class 類型信息由類加載器和類全限定名惟一肯定,
//因此想要去找這麼一個 Class 就必須提供類加載器和類全限定名,
//這個forName的重載方法容許你傳入類加載器和類全限定名來匹配方法區類型信息
//參數說明 name:class名,initialize是否加載static塊
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException{
    ..
    }   

// 這個 forName 方法默認使用調用者的類加載器,將類的.class文件加載到jvm中
//這裏傳入的initialize爲true,會去執行類中的static塊
public static Class<?> forName(String className)
    throws ClassNotFoundException {
    return forName0(className, true, ClassLoader.getCallerClassLoader());
}
複製代碼

使用:

Class clz = Class.forName("com.eft.reflect.Person");
複製代碼

2、判斷是否爲某個類的實例

  • 用 instanceof 關鍵字
  • 用 Class 對象的 isInstance 方法(Native 方法)
public class InstanceofDemo {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        if (arrayList instanceof List) {
            System.out.println("ArrayList is List");
        }
        if (List.class.isInstance(arrayList)) {
            System.out.println("ArrayList is List");
        }
    }
}
//Output:
//ArrayList is List
//ArrayList is List
複製代碼

被不一樣加載器加載過的類不屬於同一種類(即時包名、類名相同),所建立出的對象所屬的類也不相同,以下:

ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream("./bean/" + fileName);
                    if (is == null) {
                        return super.loadClass(name);//返回父 類加載器
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException();
                }
            }
        };

Object obj = null;
Class clz=myLoader.loadClass("eft.reflex.bean.Person");
System.out.println("person被自定義類加載器加載了");
obj = clz.newInstance();
System.out.println(obj instanceof Person);


運行結果:
person被自定義類加載器加載完成
person的靜態塊被調用了
false
複製代碼

原理應該是:jvm會根據instanceof右邊的操做符用默認的類加載器去加載該類到方法區,而後根據左操做符對象的對象頭中類引用地址信息去查找方法區對應的類,若是找到的類是剛剛加載的類,則結果爲true,不然爲false。對於這個例子而言,obj對象指向的類在建立對象以前就已經加載到了方法區,而進行instanceof運算時,因爲方法區中已經存在的該類並不是用此時的默認加載器進行加載,所以jvm認爲該類尚未加載,因此右側操做符指向的類此時纔會加載,因此這個例子的結果爲false。 若是將括號中的類名改成測試類的類名,結果也是相似的,只不過測試類會在main方法執行以前就會被加載。

3、經過反射獲取構造器,並建立實例對象

經過反射建立類對象主要有兩種方式:經過 Class 對象的 newInstance() 方法、經過 Constructor 對象的 newInstance() 方法。

第一種:經過 Class 對象的 newInstance() 方法。

  • public T newInstance()
    • 要求被調用的構造函數是可見,不然會拋出IllegalAccessException xxx can not access a member of class eft.reflex.Singleton with modifiers "private"的異常
    • 只可以調用無參的構造函數,即默認的構造函數

第二種:經過 Constructor 對象的 newInstance() 方法
這個操做涉及到的幾個api以下:

  • public Constructor<?>[] getConstructors() //獲取類對象的全部可見的構造函數
  • public Constructor<?>[] getDeclaredConstructors()//獲取類對象的全部的構造函數

注意: 1.getConstructors和getDeclaredConstructors獲取的構造器數組無序,因此不要經過索引來獲取指定的構造方法 2.getXXXX 與getDeclaredXXXX 區別是,帶Declared的方法不會返回父類成員,但會返回私有成員;不帶Declared的方法剛好相反下面相似的方法不贅述

  • public Constructor getConstructor(Class<?>... parameterTypes)
    • 獲取指定的可見的構造函數,參數爲:指定構造函數的參數類型數組
    • 若是該構造函數不可見或不存在,會拋出 NoSuchMethodException 異常

使用舉例:

Class p = Person.class;
Constructor constructor1 = p.getConstructor();//獲取沒有任何參數的構造函數
Constructor constructor = p.getConstructor(String.class,int.class);//獲取Person(String name,int age)這個構造函數
複製代碼
  • public Constructor getDeclaredConstructor(Class<?>... parameterTypes)
    • 獲取指定的構造函數,參數爲:指定構造函數的參數類型數組
    • 不管構造函數可見性如何,都可獲取

使用舉例:

Class p = Person.class;
Constructor constructor = p.getDeclaredConstructor(String.class,int.class);//獲取Person(String name,int age)這個構造函數
複製代碼
  • Constructor的setAccessible和newInstance方法
//關閉訪問檢查,須要先將此設置爲true纔可經過反射訪問不可見的構造器
//但編譯器不容許使用普通的代碼該字段,由於僅適用於反射
public void setAccessible(boolean flag) //建立對象,使用可變長度的參數,可是在調用構造函數時必須爲每個參數提供一個準確的參量. public T newInstance(Object ... initargs) 使用舉例: //假設Person有個 private Person(String name){}的構造方法 Constructor constructor = Person.class.getConstructor(String.class);
constructor.setAccessible(true);
Person person = (Person)constructor.newInstance("酸辣湯");
複製代碼

使用Constructor建立對象的完整例子:詳見上面的使用反射建立對象-調用類對象的構造方法

4、經過反射獲取類的屬性、方法

使用反射能夠獲取Class對象的一系列屬性和方法,接下來列舉下Class類中相關的API

類名

  • public String getName() //獲取類全路徑名(返回的是虛擬機裏面的class的表示)
  • public String getCanonicalName()//獲取類全路徑名(返回的是更容易理解的表示)
  • public String getSimpleName() //獲取不包含包名的類名

那麼以上三者區別是?舉個栗子
普通類名:

Class clz=Person.class;
System.out.println(clz);
System.out.println(clz.toString());
System.out.println(clz.getName());
System.out.println(clz.getCanonicalName());
System.out.println(clz.getSimpleName());

運行結果:
class reflex.Person class reflex.Person//Class裏面重寫了toString方法,而且在裏面調用了getName()方法 reflex.Person reflex.Person Person 複製代碼

數組:

Class clz=Person[][].class;
System.out.println(clz.getName());
System.out.println(clz.getCanonicalName());
System.out.println(clz.getSimpleName());

運行結果:
[[Lreflex.Person; 
reflex.Person[][]
Person[][]
複製代碼

修飾符

  • public native int getModifiers(); //獲取修飾符

修飾符被包裝進一個int內,每個修飾符都是一個標誌位(置位或清零)。可使用java.lang.reflect.Modifier類中的如下方法來檢驗修飾符:

Modifier.isAbstract(int mod)
    Modifier.isFinal(int mod)
    Modifier.isInterface(int mod)
    Modifier.isNative(int mod)
    Modifier.isPrivate(int mod)
    Modifier.isProtected(int mod)
    Modifier.isPublic(int mod)v
    Modifier.isStatic(int mod)
    Modifier.isStrict(int mod)//若是mod包含strictfp(strict float point (精確浮點))修飾符,則爲true; 不然爲:false。
    Modifier.isSynchronized(int mod)
    Modifier.isTransient(int mod)
    Modifier.isVolatile(int mod)
複製代碼

使用舉例:

Class clz= Person.class;
int modifier = clz.getModifiers();
System.out.println("修飾符是否爲public:" + Modifier.isPublic(modifier));

運行結果:
true
複製代碼

Modifier內部用&運算作判斷,如

public static boolean isPublic(int var0) {
    return (var0 & 1) != 0;
}
複製代碼

包信息

  • public Package getPackage()  //獲取包信息

從Package對象中你能夠訪問諸如名字等包信息。您還能夠訪問類路徑上這個包位於JAR文件中Manifest這個文件中指定的信息。例如,你能夠在Manifest文件中指定包的版本號。能夠在java.lang.Package中瞭解更多包類信息。

父類

  • public native Class<? super T> getSuperclass(); //獲取直接父類

父類的Class對象和其它Class對象同樣是一個Class對象,能夠繼續使用反射

實現的接口

  • public native Class<?>[] getInterfaces(); //獲取實現的接口列表
  • 一個類能夠實現多個接口。所以返回一個Class數組。在Java反射機制中,接口也由Class對象表示。
  • 注意:只有給定類聲明實現的接口才會返回。例如,若是類A的父類B實現了一個接口C,但類A並無聲明它也實現了C,那麼C不會被返回到數組中。即便類A實際上實現了接口C,由於它的父類B實現了C。

爲了獲得一個給定的類實現接口的完整列表,須要遞歸訪問類和其超類

  • public Type[] getGenericInterfaces() //getGenericInterface返回包括泛型的類型

字段

  • public Field[] getFields() //獲取全部可見的字段信息,Field數組爲類中聲明的每個字段保存一個Field 實例
  • public Field[] getDeclaredFields()//獲取全部的字段信息
  • public Field getField(String name) //經過字段名稱獲取字符信息,該字段必須可見,不然拋出異常
  • public Field getDeclaredField(String name) //經過字段名稱獲取可見的字符信息

關於Field
  • public String getName() //獲取字段名字
  • public Class<?> getType() //獲取一個字段的類型

使用舉例:

Class clz = Person.class;
Field field = clz.getDeclaredField("name");

System.out.println("獲取字段名稱:" + field.getName());
System.out.println("獲取字段類型:" +field.getType());

運行結果:
獲取字段名稱:name
獲取字段類型:class java.lang.String 複製代碼
  • public Object get(Object obj) //獲取字段的值
  • public void set(Object obj, Object value)//設置字段的值,

注意:

  1. 若是獲取的字段不可見,則再經過set和get訪問以前,必須先使用 setAccessible(true) 設置爲可訪問
  2. 若是是靜態字段,obj傳入null,而不是具體的對象;不過,若是傳具體的對象也是能正常操做的

使用舉例:

Person person= (Person) clz.newInstance();
field.setAccessible(true);//設置爲可訪問
field.set(person, "酸辣湯");//經過set方法設置字段值
System.out.println("經過get獲取的值:"+field.get(person));

運行結果:
經過get獲取的值:酸辣湯
複製代碼

當反射遇到final修飾的字段

看以下例子:

public class FinalTest {

    public static void main(String[] args )throws Exception {
        Field nameField = OneCity.class.getDeclaredField("name");

        nameField.setAccessible(true);
        nameField.set(null, "Shenzhen");
        System.out.println(OneCity.getName());

    }
}

class OneCity {
    private static final String name = new String("Beijing");

    public static String getName() {
        return name;
    }
}

複製代碼

輸出結果:

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final java.lang.String field eft.reflex.OneCity.name to java.lang.String
複製代碼

那麼該如何用反射來修改它的值?

這時候咱們要作一個更完全的反射 — 對 Java 反射包中的類進行自我反射。Field 對象有個一個屬性叫作 modifiers, 它表示的是屬性是不是 public, private, static, final 等修飾的組合。這裏把這個 modifiers 也反射出來,進而把 nameField 的 final 約束也去掉了,回到了上面的情況了。完整代碼是這樣的:

public class FinalTest {

    public static void main(String[] args )throws Exception {
        Field nameField = OneCity.class.getDeclaredField("name");

        Field modifiersField = Field.class.getDeclaredField("modifiers"); //①
        modifiersField.setAccessible(true);
        modifiersField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); //②

        nameField.setAccessible(true); //這個一樣不能少,除非上面把 private 也拿掉了,可能還得 public
        nameField.set(null, "Shenzhen");
        System.out.println(OneCity.getName()); //輸出 Shenzhen

    }
}

class OneCity {
    private static final String name = new String("Beijing");

    public static String getName() {
        return name;
    }
}
複製代碼

在 ① 處把 Field 的 modifiers 找到,它也是個私有變量,因此也要 setAccessible(ture)。接着在 ② 處把 nameField 的 modifiers 值改掉,是用的按位取反 ~ 再按位與 ~ 操做把 final 從修飾集中剔除掉,其餘特性如 private, static 保持不變。再想一下 modifierField.setInt() 能夠把 private 改成 public, 如此則修改 name 時無需 setAccessible(true) 了。

經過把屬性的 final 去掉, 就成功把 name 改爲了 Shenzhen。

注意上面爲什麼把 OneCity 的 name 賦值爲 new String(「Beijing」), 這是爲了避免讓 Java 編譯器內聯 name 到 getName() 方法中,而使 getName() 的方法體爲 return 「Beijing」,形成 getName() 永遠輸出 」Beijing」 。

方法

  • public Method[] getMethods() 獲取全部可見的方法
  • public Method[] getDeclaredMethods() 獲取全部的方法,不管是否可見
  • public Method getMethod(String name, Class<?>... parameterTypes)
    • 經過方法名稱、參數類型獲取方法
    • 若是你想訪問的方法不可見,會拋出異常
    • 若是你想訪問的方法沒有參數,傳遞 null做爲參數類型數組,或者不傳值)
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    • 經過方法名稱、參數類型獲取方法
    • 若是你想訪問的方法沒有參數,傳遞 null做爲參數類型數組,或者不傳值)

關於Method
  • public Class<?>[] getParameterTypes() //獲取方法的全部參數類型
  • public Class<?> getReturnType() //獲取方法的返回值類型
  • public Object invoke(Object obj, Object... args)//調用方法
    • obj:想要調用該方法的對象;args:方法的具體參數,必須爲每一個參數提供一個準確的參量
    • 若是方法是靜態的,這裏obj傳入null
    • 若是方法沒有參數,args傳null或者不傳

使用舉例:

Person類裏有這麼一個方法:
private void testMethod(String param){
    System.out.println("調用了testMethod方法,參數是:"+param);
}



//經過反射調用方法
Class clz = Person.class;
Method method=clz.getDeclaredMethod("testMethod",String.class);
method.setAccessible(true);
method.invoke(clz.newInstance(),"我是具體的參數值");

運行結果:
調用了testMethod方法,參數是:我是具體的參數值
複製代碼

關於Method更多API自行查看源碼

使用Java反射能夠在運行時檢查類的方法並調用它們。這能夠用來檢測一個給定的類有哪些get和set方法。能夠經過掃描一個類的全部方法並檢查每一個方法是不是get或set方法。
下面是一段用來找到類的get和set方法的代碼:

public static void printGettersSetters(Class aClass){

        Method[]methods = aClass.getMethods();

        for(Methodmethod : methods){
           if(isGetter(method))System.out.println("getter: " + method);
           if(isSetter(method))System.out.println("setter: " + method);
        }
}

public staticboolean isGetter(Method method){
        if(!method.getName().startsWith("get"))      return false;
        if(method.getParameterTypes().length!= 0)   return false; 
        if(void.class.equals(method.getReturnType())return false;
        return true;

}

public staticboolean isSetter(Method method){
    if(!method.getName().startsWith("set"))return false;
    if(method.getParameterTypes().length!= 1) return false;
    return true;
}
複製代碼

註解

  • public Annotation[] getAnnotations() //獲取當前成員全部的註解,不包括繼承的;(since jdk1.5)
  • public Annotation[] getDeclaredAnnotations()//獲取包括繼承的全部註解;(since jdk1.5)

關於註解,下回詳解

反射與數組

數組:定義多個類型相同的變量
咱們都知道,數組是一種特殊的類型,它本質上由虛擬機在運行時動態生成,因此在反射這種類型的時候會稍有不一樣。
由於數組類直接由虛擬機運行時動態建立,因此你不可能從一個數組類型的 Class 實例中獲得構造方法,編譯器根本沒機會爲類生成默認的構造器。因而你也不能以常規的方法經過 Constructor 來建立一個該類的實例對象。
若是你非要嘗試使用 Constructor 來建立一個新的實例的話,那麼運行時程序將告訴你沒法匹配一個構造器。像這樣:

Class<String[]> cls = String[].class;
Constructor constructor = cls.getConstructor();
String[] strs = (String[]) constructor.newInstance();
複製代碼

程序會拋出 NoSuchMethodException的異常,告訴你Class 實例中根本找不到一個無參的構造器

那咱們要怎麼動態建立一個數組??
Java 中有一個類 java.lang.reflect.Array 提供了一些靜態的方法用於動態的建立和獲取一個數組類型

  • public static Object newInstance(Class<?> componentType, int length)
    • //建立一個一維數組,componentType 爲數組元素類型,length 數組長度
  • public static Object newInstance(Class<?> componentType, int... dimensions)
    • //可變參數 dimensions,指定多個維度的單維度長度
  • public static native void set(Object array, int index, Object value)
    • 把數組array索引位置爲index的設爲value值
  • public static native Object get(Object array, int index)
    • 得到數組array的index位置上的元素

補充下,Class類中獲取組件類型的API:

  • public native Class<?> getComponentType();
    • 若是class是數組類型, 獲取其元素的類型,若是是非數組,則返回null

一維數組實例

//用反射來定義一個int類型,3長度的數組
int[] intArray = (int[]) Array.newInstance(int.class, 3);

Array.set(intArray, 0, 123);
Array.set(intArray, 1, 456);
Array.set(intArray, 2, 789);

System.out.println("intArray[0] = " + Array.get(intArray, 0));
System.out.println("intArray[1] = " + Array.get(intArray, 1));
System.out.println("intArray[2] = " + Array.get(intArray, 2));

//獲取類對象的一個數組
Class stringArrayClass = Array.newInstance(int.class, 0).getClass();
System.out.println("is array: " + stringArrayClass.isArray());


//獲取數組的組件類型
String[] strings = new String[3];
Class stringArrayClass2 = strings.getClass();
Class stringArrayComponentType = stringArrayClass2.getComponentType();
System.out.println(stringArrayComponentType);

運行結果:
intArray[0] = 123
intArray[1] = 456
intArray[2] = 789
is array: true
class java.lang.String 複製代碼

多維數組:

// 建立一個三維數組,每一個維度長度分別爲5,10,15
int[] dims = new int[] { 5, 10,15 };
Person[][][] array = (Person[][][]) Array.newInstance(Person.class, dims); // 可變參數,也能夠這樣寫:Object array = Array.newInstance(Integer.TYPE, 5,10,15);

Class<?> classType0 = array.getClass().getComponentType();    // 返回數組元素類型
System.out.println("三維數組元素類型:"+classType0);    // 三維數組的元素爲二維數組

Object arrayObject = Array.get(array, 2);// 得到三維數組中索引爲2的元素,返回的是一個二維數組
System.out.println("二維數組元素類型:"+arrayObject.getClass().getComponentType());

Object oneObject = Array.get(arrayObject, 0);// 得到二維數組中索引爲0的數組,返回的是一個一維數組
System.out.println("一維數組元素類型:"+oneObject.getClass().getComponentType());

Array.set(oneObject,14,new Person("酸辣湯",18));//設置覺得數組索引爲3的位置的元素

System.out.println("未被設置元素的位置:"+array[0][0][0]);
System.out.println("已被設置元素的位置:"+array[2][0][14]);

運行結果:
三維數組元素類型:class [[Left.reflex.bean.Person;
二維數組元素類型:class [Left.reflex.bean.Person;
一維數組元素類型:class eft.reflex.bean.Person
person的靜態塊被調用了
未被設置元素的位置:null
已被設置元素的位置:Person{name='酸辣湯', age=18}
複製代碼

反射與泛型

泛型是 Java 編譯器範圍內的概念,它可以在程序運行以前提供必定的安全檢查,而反射是運行時發生的,也就是說若是你反射調用一個泛型方法,實際上就繞過了編譯器的泛型檢查了。咱們看一段代碼:

ArrayList<Integer> list = new ArrayList<>();
list.add(23);
//list.add("fads");編譯不經過

Class<?> cls = list.getClass();
Method add = cls.getMethod("add",Object.class);
add.invoke(list,"hello");
for (Object obj:list){
    System.out.println(obj);
}

運行結果:
23
hello
複製代碼

最終你會發現咱們從整型容器中取出一個字符串,由於虛擬機只管在運行時從方法區找到 ArrayList 這個類的類型信息並解析出它的 add 方法,接着執行這個方法。它不像通常的方法調用,調用以前編譯器會檢測這個方法存在不存在,參數類型是否匹配等,因此沒了編譯器的這層安全檢查,反射地調用方法更容易遇到問題。

使用反射來獲取泛型信息

在實際應用中,爲了得到和泛型有關的信息,Java就新增了幾種類型來表明不能被歸一到Class類中的類型,但又和基本數據類型齊名的類型,一般使用的是以下兩個:

  • GenericType: 表示一種元素類型是參數化的類型或者類型變量的數組類型。@since 1.5
  • ParameterizedType: 表示一種參數化的類型。    @since 1.5

爲何要引入這兩種呢,實際上,在經過反射得到成員變量時,Field類有一個方法是getType,能夠得到該字段的屬性,可是這種屬性若是是泛型就獲取不到了,因此才引入了上面兩種類型。

實例:

public class Person {
    ...
    private Map<String,Integer> map;     
    ...
}

Class<Person> clazz = Person.class;
Field f = clazz.getDeclaredField("map");

//經過getType方法只能得到普通類型
System.out.println("map的類型是:" + f.getType()); //打印Map

//1. 得到f的泛型類型
Type gType = f.getGenericType();

//2.若是gType是泛型類型對像
if(gType instanceof ParameterizedType)
{
    ParameterizedType pType = (ParameterizedType)gType;
    //獲取原始類型
    Type rType = pType.getRawType();
    System.out.println("原始類型是: " + rType);

    //得到泛型類型的泛型參數
    Type[] gArgs = pType.getActualTypeArguments();
    //打印泛型參數
    for(int i=0; i < gArgs.length; i ++)
    {
        System.out.println("第"+ i +"個泛型類型是:" + gArgs[i]);
    }
}
else {
    System.out.println("獲取泛型信息失敗");

}

運行結果:
map的類型是:interface java.util.Map
原始類型是: interface java.util.Map
第0個泛型類型是:class java.lang.String
第1個泛型類型是:class java.lang.Integer
複製代碼

反射源碼與性能開銷

只列舉個別方法的源碼,其餘的有興趣能夠自行查看源碼(大部分都是native方法)

調用invoke()方法

獲取到Method對象以後,調用invoke方法的流程以下:

能夠看到,調用Method.invoke以後,會直接去調MethodAccessor.invoke。MethodAccessor就是上面提到的全部同名method共享的一個實例,由ReflectionFactory建立。建立機制採用了一種名爲inflation的方式(JDK1.4以後):若是該方法的累計調用次數<=15,會建立出NativeMethodAccessorImpl,它的實現就是直接調用native方法實現反射;若是該方法的累計調用次數>15,會由java代碼建立出字節碼組裝而成的MethodAccessorImpl。(是否採用inflation和15這個數字均可以在jvm參數中調整)
以調用MyClass.myMethod(String s)爲例,生成出的MethodAccessorImpl字節碼翻譯成Java代碼大體以下:

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {    
   public Object invoke(Object obj, Object[] args) throws Exception {
       try {
           MyClass target = (MyClass) obj;
           String arg0 = (String) args[0];
           target.myMethod(arg0);
       } catch (Throwable t) {
           throw new InvocationTargetException(t);
       }
   }
}
複製代碼

至於native方法的實現,因爲比較深刻本文就不探討了

直接調用方法與經過反射調用方法對比

public static void main(String[] args) throws Exception {
       // directCall();//直接調用
        reflectCall();//反射調用
    }

    public static void target(int i) {
    }

    //直接調用
    private static void directCall() {
        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            MethodTest.target(128);
        }
    }

    //反射調用同一個方法
    private static void reflectCall() throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }
    
運行結果:
直接調用結果:
...
121
126
105
115
100 (取最後5個值,做爲預熱後的峯值性能)

反射調用結果:
...
573
581
593
557
594 (取最後5個值,做爲預熱後的峯值性能)

複製代碼

結果分析:普通調用做爲性能基準,大約100多秒,經過反射調用的耗時大約爲基準的4倍

爲什麼反射會帶來性能開銷?

先看下使用反射調用的字節碼文件:

63: aload_1                         // 加載Method對象
64: aconst_null                     // 靜態方法,反射調用的第一個參數爲null
65: iconst_1
66: anewarray                       // 生成一個長度爲1的Object數組
69: dup
70: iconst_0
71: sipush        128
74: invokestatic Integer.valueOf    // 將128自動裝箱成Integer
77: aastore                         // 存入Object數組
78: invokevirtual Method.invoke     // 反射調用
複製代碼

能夠看出反射調用前的兩個動做

  • Method.invoke是一個變長參數方法,最後一個參數在字節碼層面會是Object數組
    • Java編譯器會在方法調用處生成一個長度爲入參數量的Object數組,並將入參一一存儲進該數組
  • Object數組不能存儲基本類型,Java編譯器會對傳入的基本類型進行自動裝箱

上述兩個步驟會帶來性能開銷和GC

如何下降開銷?

    1. 增長啓動JVM參數:-Djava.lang.Integer.IntegerCache.high=128,減小裝箱

    經測試,峯值性能:280.4ms,爲基準耗時的2.5倍

    1. 減小自動生成Object數組,測試代碼以下:
private static void reflectCall() throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);

        // 在循環外構造參數數組
        Object[] arg = new Object[1];
        arg[0] = 128;

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }
複製代碼

字節碼:

80: aload_2                         // 加載Method對象
81: aconst_null                     // 靜態方法,反射調用的第一個參數爲null
82: aload_3
83: invokevirtual Method.invoke     // 反射調用,無anewarray指令
複製代碼

經測試,峯值性能:312.4ms,爲基準耗時的2.8倍

    1. 關閉inflation機制
    • -Dsun.reflect.noInflation=true,關閉Inflation機制,反射調用在一開始便會直接使用動態實現,而不會使用委派實現或者本地實現 (即一開始invoke方法就使用java實現的而不使用native方法)
    • 關閉權限校驗:每次反射調用都會檢查目標方法的權限
// -Djava.lang.Integer.IntegerCache.high=128
// -Dsun.reflect.noInflation=true
public static void main(String[] args) throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);
        // 關閉權限檢查
        method.setAccessible(true);

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }
複製代碼

峯值性能:186.2ms,爲基準耗時的1.7倍

反射優缺點

優勢

1.增長程序的靈活性,避免將程序寫死到代碼裏。

例:定義了一個接口,實現這個接口的類有20個,程序裏用到了這個實現類的地方有好多地方,若是不使用配置文件手寫的話,代碼的改動量很大,由於每一個地方都要改並且不容易定位,若是你在編寫以前先將接口與實現類的寫在配置文件裏,下次只需改配置文件,利用反射(java API已經封裝好了,直接用就能夠用 Class.newInstance())就可完成

2.代碼簡潔,提升代碼的複用率,外部調用方便

缺點

  • 性能開銷 - 因爲反射涉及動態解析的類型,所以沒法執行某些 Java 虛擬機優化。所以,反射操做的性能要比非反射操做的性能要差,應該在性能敏感的應用程序中頻繁調用的代碼段中避免。
  • 破壞封裝性 - 反射調用方法時能夠忽略權限檢查,所以可能會破壞封裝性而致使安全問題。
  • 模糊程序內部邏輯 - 程序人員但願在源代碼中看到程序的邏輯,反射等繞過了源代碼的技術,於是會帶來維護問題。反射代碼比相應的直接代碼更復雜。
  • 內部曝光 - 因爲反射容許代碼執行在非反射代碼中非法的操做,例如訪問私有字段和方法,因此反射的使用可能會致使意想不到的反作用,這可能會致使代碼功能失常並可能破壞可移植性。反射代碼打破了抽象,所以可能會隨着平臺的升級而改變行爲。

Java反射能夠訪問和修改私有成員變量,那封裝成private還有意義麼?

既然小偷能夠訪問和搬走私有成員傢俱,那封裝成防盜門還有意義麼?這是同樣的道理,而且Java從應用層給咱們提供了安全管理機制——安全管理器,每一個Java應用均可以擁有本身的安全管理器,它會在運行階段檢查須要保護的資源的訪問權限及其它規定的操做權限,保護系統免受惡意操做攻擊,以達到系統的安全策略。因此其實反射在使用時,內部有安全控制,若是安全設置禁止了這些,那麼反射機制就沒法訪問私有成員。

反射是否真的會讓你的程序性能下降?

1.反射大概比直接調用慢50~100倍,可是須要你在執行100萬遍的時候纔會有所感受
2.判斷一個函數的性能,你須要把這個函數執行100萬遍甚至1000萬遍
3.若是你只是偶爾調用一下反射,請忘記反射帶來的性能影響
4.若是你須要大量調用反射,請考慮緩存。
5.你的編程的思想纔是限制你程序性能的最主要的因素

開發中使用反射的場景

工廠模式:Factory類中用反射的話,添加了一個新的類以後,就不須要再修改工廠類Factory了,以下例子

數據庫JDBC中經過Class.forName(Driver).來得到數據庫鏈接驅動

開發通用框架 - 反射最重要的用途就是開發各類通用框架。不少框架(好比 Spring)都是配置化的(好比經過 XML 文件配置 JavaBean、Filter 等),爲了保證框架的通用性,它們可能須要根據配置文件加載不一樣的對象或類,調用不一樣的方法,這個時候就必須用到反射——運行時動態加載須要加載的對象。

動態代理 - 在切面編程(AOP)中,須要攔截特定的方法,一般,會選擇動態代理方式。這時,就須要反射技術來實現了。

註解 - 註解自己僅僅是起到標記做用,它須要利用反射機制,根據註解標記去調用註解解釋器,執行行爲。若是沒有反射機制,註解並不比註釋更有用。

可擴展性功能 - 應用程序能夠經過使用徹底限定名稱建立可擴展性對象實例來使用外部的用戶定義類。

使用反射的工廠模式舉例:

//用反射機制實現工廠模式:
interface fruit{
    public abstract void eat();
}
class Apple implements fruit{
    public void eat(){
        System.out.println("Apple");
    }
}
class Orange implements fruit{
    public void eat(){
        System.out.println("Orange");
    }
}
class Factory{
    public static fruit getInstance(String ClassName){
        fruit f=null;
        try{
            f=(fruit)Class.forName(ClassName).newInstance();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}

客戶端:
class hello{
    public static void main(String[] a){
        fruit f=Factory.getInstance("Reflect.Apple");
        if(f!=null){
            f.eat();
        }
    }
}
複製代碼

反射與內省

內省(自省):
內省基於反射實現,也就是對反射的再次包裝,主要用於操做JavaBean,經過內省 能夠獲取bean的getter/setter
通俗地說:javaBean 具備的自省機制能夠在不知道javaBean都有哪些屬性的狀況下,設置他們的值。核心也是反射機制

通常在開發框架時,當須要操做一個JavaBean時,若是一直用反射來操做,顯得很麻煩;因此sun公司開發一套API專門來用來操做JavaBean

內省是 Java 語言對 Bean 類屬性、事件的一種缺省處理方法。例如類 A 中有屬性 name, 那咱們能夠經過 getName,setName 來獲得其值或者設置新的值。經過 getName/setName 來訪問 name 屬性,這就是默認的規則。 Java 中提供了一套 API 用來訪問某個屬性的 getter/setter 方法,經過這些 API 可使你不須要了解這個規則(但你最好仍是要搞清楚),這些 API 存放於包 java.beans 中。
通常的作法是經過類 Introspector 來獲取某個對象的 BeanInfo 信息,而後經過 BeanInfo 來獲取屬性的描述器( PropertyDescriptor ),經過這個屬性描述器就能夠獲取某個屬性對應的 getter/setter 方法,而後咱們就能夠經過反射機制來調用這些方法。下面咱們來看一個例子,這個例子把某個對象的全部屬性名稱和值都打印出來:

package introspector;
//這些api都是在java.beans下(rt.jar包下)
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class IntrospectorDemo{
    String name;
    public static void main(String[] args) throws Exception{
        IntrospectorDemo demo = new IntrospectorDemo();
        // 若是不想把父類的屬性也列出來的話,
        //那 getBeanInfo 的第二個參數填寫父類的信息
        BeanInfo bi = Introspector.getBeanInfo(demo.getClass(), Object. class );//Object類是全部Java類的根父類
        PropertyDescriptor[] props = bi.getPropertyDescriptors();//得到屬性的描述器
        for ( int i=0;i<props.length;i++){
            System.out.println("獲取屬性的Class對象:"+props[i].getPropertyType());
            props[i].getWriteMethod().invoke(demo, "酸辣湯" );//得到setName方法,並使用invoke調用
            System.out.println("讀取屬性值:"+props[i].getReadMethod().invoke(demo, null ));
        }
    }

    public String getName() {
        return name;
    }

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

運行結果:
獲取屬性的Class對象:class java.lang.String 讀取屬性值:酸辣湯 複製代碼

JDK內省類庫:PropertyDescriptor類:
 PropertyDescriptor類表示JavaBean類經過存儲器導出一個屬性。主要方法:
1. getPropertyType(),得到屬性的Class對象;
2. getReadMethod(),得到用於讀取屬性值的方法;(如上面的獲取getName方法)
3.getWriteMethod(),得到用於寫入屬性值的方法;(如上面的獲取setName方法)
4.hashCode(),獲取對象的哈希值;
5. setReadMethod(Method readMethod),設置用於讀取屬性值的方法;
6. setWriteMethod(Method writeMethod),設置用於寫入屬性值的方法。

Apache開發了一套簡單、易用的API來操做Bean的屬性——BeanUtils工具包。

參考資料

zhuanlan.zhihu.com/p/34168509

fanyilun.me/2015/10/29/…

zhongmingmao.me/2018/12/20/…

相關文章
相關標籤/搜索