java反射全解

酸辣湯發表於2019-08-03

引言

java中建立物件有幾種方式?

1.使用new關鍵字

2.使用clone方法

3.使用反序列化

4.使用反射

5.使用Unsafe

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

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

反射簡介

反射之中包含了一個「反」字,所以想要解釋反射就必須先從「正」開始解釋。
一般情況下,我們使用某個類時必定知道它是什麼類,是用來做什麼的。於是我們直接對這個類進行例項化,之後使用這個類物件進行操作。

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)。

所以說什麼是反射?

反射就是在執行時才知道要操作的類是什麼,並且可以在執行時獲取類的完整構造,並呼叫對應的方法。

官方定義

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

反射機制很重要的一點就是“執行時”,其使得我們可以在程式執行時載入、探索以及使用編譯期間完全未知的 .class 檔案。換句話說,Java 程式可以載入一個執行時才得知名稱的 .class 檔案,然後獲悉其完整構造,並生成其物件實體、或對其 fields(變數)設值、或呼叫其 methods(方法)。

通俗概括

反射就是讓你可以通過名稱來得到物件的資訊(如類,屬性,方法等) 的技術

核心

Java反射機制是Java語言被視為“準動態”語言的關鍵性質。Java反射機制的核心就是允許在執行時通過Java Reflection APIs來取得已知名字的class類的內部資訊(包括其modifiers(諸如public, static等等)、superclass(例如Object)、實現interfaces(例如Serializable),也包括fields和methods的所有資訊),動態地生成此類,並呼叫其方法或修改其域(甚至是本身宣告為private的域或方法)

功能

  • 在執行時判斷任意一個物件所屬的類;
  • 在執行時構造任意一個類的物件;
  • 在執行時判斷任意一個類所具有的成員變數和方法;
  • 在執行時呼叫任意一個物件的方法;
  • 生成動態代理

反射原理

類載入的流程:

java反射全解

類載入的完整過程如下:
(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個方面:

一、獲取反射的 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");
複製程式碼

二、判斷是否為某個類的例項

  • 用 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方法執行之前就會被載入。

三、通過反射獲取構造器,並建立例項物件

通過反射建立類物件主要有兩種方式:通過 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建立物件的完整例子:詳見上面的使用反射建立物件-呼叫類物件的構造方法

四、通過反射獲取類的屬性、方法

使用反射可以獲取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方法的流程如下:

java反射全解

可以看到,呼叫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/…

相關文章