1. 程式人生 > >Kotlin/Java中的反射詳解

Kotlin/Java中的反射詳解

什麼是反射

反射是一種計算機處理方式。有程式可以訪問、檢測和修改它本身狀態或行為的這種能力。能提供封裝程式集、型別的物件。
對於Java這種OOP語言來講,執行狀態中,我們可以根據“類的部分資訊”來還原“類的全部資訊”,這就是Java中的反射。

Java虛擬機器的體系結構

Java虛擬機器遮蔽了與具體作業系統平臺相關的資訊,使得Java程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。通俗地說Java虛擬機器就是處理Java程式(確切地說是Java位元組碼)的虛擬機器。
作為虛擬機器,JVM的結構和常見的作業系統一致,有著自己的堆、棧、方法區、PC計數器和指令系統。它的結構如下圖所示:
這裡我們暫時不去談類載入子系統與執行引擎,只談一下Java執行時的資料區,它由五個部分組成:


image.png

(1)程式計數器(執行緒私有)
程式計數器是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,
分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。
因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的執行緒計數器,各條執行緒之間的計數器互不影響,獨立儲存。
(2)虛擬機器棧(執行緒私有)
在Java(或者其他JVM的語言)每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。
每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
Java虛擬機器棧存放區域性變量表,如編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別,它不等同於物件本身,根據不同的虛擬機器實現,它可能是一個指向物件起始地址的引用指標,也可能指向一個代表物件的控制代碼或者與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。
這也導致在Java中匿名內部類來自外部閉包環境的自由變數必須是final的(Java編譯器是capture-by-value模式),不過在Kotlin中則沒有此限制,它通過自動包裝實現了capture-by-reference(所以它沒有基本型別)。
(3)本地方法棧(執行緒私有)
與虛擬機器棧的作用相似,其區別為虛擬機器棧執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器所使用的Native方法。
(4)堆(執行緒共享)
是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域唯一的目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”(Garbage Collected Heap)。由於現在收集器基本都是採用的分代收集演算法,所以Java堆中還可以細分為:新生代和老年代。
(5)方法區

(執行緒共享)
方法區用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
具體來講,對於我們在Java程式中使用的每一個類,它都會在方法區生成一個對應的class檔案,這個檔案記錄的資訊有:
(如果你想了解更多的資訊,可以參考http://blog.csdn.net/luanlouis/article/details/39892027),這裡只簡單的說明一下。
1.類資訊
2.欄位資訊
3.方法資訊
4.常量池
5.類變數(靜態static欄位,或者companion object)
6.classLoader的引用
7.class物件的引用
8.方法表
正由於在JVM的方法區中實時記錄了這些資訊,我們才可以在執行時獲取類的全部資訊,其關鍵在於獲取其對應的Class物件。

獲取Class物件

在Java中,獲取Class物件有以下幾種方法:
1: Class.forName("類名字串") (注意:類名字串必須是全稱,包名+類名)
2: 類名.class
3: 例項物件.getClass()

    public void getClassTest()
    {
       try{
           Class baseInfo = Class.forName("com.suiseiseki.www.BaseInfo");
           Class object = Object.class;
           Class date = (new Date()).getClass();
           Class testclass = this.getClass();
       }
       catch (Exception e)
       {
           e.printStackTrace();
       }
    }

還原類的資訊

獲取類的構造器

Java提供以下Api用於獲取類的構造方法:

// 獲取“引數是parameterTypes”的public的建構函式
public Constructor    getConstructor(Class[] parameterTypes)
// 獲取全部的public的建構函式
public Constructor[]    getConstructors()
// 獲取“引數是parameterTypes”的,並且是類自身宣告的建構函式,包含public、protected和private方法。
public Constructor    getDeclaredConstructor(Class[] parameterTypes)
// 獲取類自身宣告的全部的建構函式,包含public、protected和private方法。
public Constructor[]    getDeclaredConstructors()
// 如果這個類是“其它類的建構函式中的內部類”,呼叫getEnclosingConstructor()就是這個類所在的建構函式;若不存在,返回null。
public Constructor    getEnclosingConstructor()

例如:

public class Test1 {

    public void testConstructor()
    {
       try{
           Class c = Country.class;
           //獲取public的無引數構造器
           Constructor origin = c.getDeclaredConstructor();
           //獲取private的構造器(注意int.class不是Integer.class)
           Constructor cst2 = c.getDeclaredConstructor(new Class[]{int.class,int.class});
           //構造器是private的,所以這裡要設定為可訪問
           cst2.setAccessible(true);

           Country c1 = (Country)origin.newInstance();
           Country c2 = (Country)cst2.newInstance(30,100);
           System.out.println(c1);
           System.out.println(c2);
       }
       catch (Exception e) {}
    }
}
class Country{
    public int pop;
    public int money;
    public Country(){
        pop = 0;
        money = 0;
    }
    private Country(int pop,int money)
    {
        this.pop = pop;
        this.money = money;
    }
    @Override
    public String toString()
    {
        return "pop "+pop+" money: "+money;
    }

    public void doublePop()
    {
        this.pop = this.pop * 2;
    }

    private int multiMoney(int n)
    {
        this.money = this.money*n;
        return this.money;
    }

}

在獲取到構造器後,可以呼叫構造器的newInstance建立物件。例子中可以看到,反射是可以訪問類的private域的,而且還可以修改它,使用被隱藏的構造器。

獲取類的方法

在Java中,方法是作為Method物件包裝的,獲取類的方法物件的Api如下:

// 獲取“名稱是name,引數是parameterTypes”的public的函式(包括從基類繼承的、從介面實現的所有public函式)
public Method    getMethod(String name, Class[] parameterTypes)
// 獲取全部的public的函式(包括從基類繼承的、從介面實現的所有public函式)
public Method[]    getMethods()
// 獲取“名稱是name,引數是parameterTypes”,並且是類自身宣告的函式,包含public、protected和private方法。
public Method    getDeclaredMethod(String name, Class[] parameterTypes)
// 獲取全部的類自身宣告的函式,包含public、protected和private方法。
public Method[]    getDeclaredMethods()
// 如果這個類是“其它類中某個方法的內部類”,呼叫getEnclosingMethod()就是這個類所在的方法;若不存在,返回null。
public Method    getEnclosingMethod()

例如,以下方法獲取方法並呼叫:

    public void testMethod()
    {
        try{
            Class c = Country.class;
            Country country3 = new Country();
            country3.money = 100;
            country3.pop = 3;
            //獲取public方法(無引數,無返回值)
            Method mDoublePop = c.getMethod("doublePop",new Class[]{});
            //呼叫invoke執行方法,需要傳入一個該類的物件
            mDoublePop.invoke(country3);
            System.out.println(country3);
            //獲取public方法(有引數,有返回值)
            Method mMultimoney = c.getMethod("multiMoney", new Class[]{int.class});
            mMultimoney.setAccessible(true);
            mMultimoney.invoke(country3,42);
            System.out.println(country3);
        }
        catch (Exception e) {}
    }

對應方法:

    public void doublePop()
    {
        this.pop = this.pop * 2;
    }

    private int multiMoney(int n)
    {
        this.money = this.money*n;
        return this.money;
    }

獲取類的成員變數

在Java中,成員變數稱為Field物件,獲取類成員變數的Api如下:

// 獲取“名稱是name”的public的成員變數(包括從基類繼承的、從介面實現的所有public成員變數)
public Field    getField(String name)
// 獲取全部的public成員變數(包括從基類繼承的、從介面實現的所有public成員變數)
public Field[]    getFields()
// 獲取“名稱是name”,並且是類自身宣告的成員變數,包含public、protected和private成員變數。
public Field    getDeclaredField(String name)
// 獲取全部的類自身宣告的成員變數,包含public、protected和private成員變數。
public Field[]    getDeclaredFields()

例如:

    public void fieldTest()
    {
        try{
            Class c = Country.class;
            Country country4 = new Country();
            country4.money = 100;
            country4.pop = 3;

            Field fPop = c.getField("pop");
            fPop.set(42,country4);
            System.out.println(country4);
        }
        catch (Exception e) {}
    }

注意許可權的問題,如果沒有許可權,需要setAccessible(true),否則會丟擲異常

類的其它資訊

1.註解

// 獲取類的"annotationClass"型別的註解 (包括從基類繼承的、從介面實現的所有public成員變數)
public Annotation<A>    getAnnotation(Class annotationClass)
// 獲取類的全部註解 (包括從基類繼承的、從介面實現的所有public成員變數)
public Annotation[]    getAnnotations()
// 獲取類自身宣告的全部註解 (包含public、protected和private成員變數)
public Annotation[]    getDeclaredAnnotations()

現在,我們可以編寫一些程式來自動處理程式中的註解了,例如根據註解來決定是否處理一個類:

    public void handleMyAnnotation(List<Class> list)
    {
        for(Class clazz : list)
        {
            if(clazz.getAnnotation(MyAnnotation.class))
            {
                println("This class is under Annotation");
            }
        }
    }

很多著名的開源庫(Dagger2,GSON,Retrofit,AspectJ)等都是通過反射+註解完成的,這些庫在方便了編寫程式的同時也會帶來一些效能開銷(儘管它們自身已經盡力地做了優化),
反射在把裝載期做的事情搬到了執行期,因此編譯器沒法對反射相關的程式碼做優化。

2.介面和基類

// 獲取實現的全部介面
public Type[]    getGenericInterfaces()
// 獲取基類
public Type    getGenericSuperclass()

注意反射獲取的基類是直接的基類(也就是說只能獲取上一級),要獲取繼承鏈,需要進一步深度遍歷

3.描述性資訊

// 獲取“類名”
public String    getSimpleName()
// 獲取“完整類名”
public String    getName()
// 類是不是“列舉類”
public boolean    isEnum()
// obj是不是類的物件
public boolean    isInstance(Object obj)
// 類是不是“介面”
public boolean    isInterface()
// 類是不是“本地類”。本地類,就是定義在方法內部的類。
public boolean    isLocalClass()
// 類是不是“成員類”。成員類,是內部類的一種,但是它不是“內部類”或“匿名類”。
public boolean    isMemberClass()
// 類是不是“基本型別”。 基本型別,包括void和boolean、byte、char、short、int、long、float 和 double這幾種型別。
public boolean    isPrimitive()

在Kotlin中使用Java中的反射

作為基於JVM的語言,Kotlin當然也支援Java語言中原有的反射機制(而且程式碼量往往更少),通過類的javaClass實現。
例如,一個常見的通過反射來獲取R檔案中控制元件的ID描述的程式:

    val viewId : String by lazy {
            val c = R.id()
            val fields = c.javaClass.declaredFields
            val r = fields.find { it.getInt(c) == view.id }?.name?:"Not found"
            r
    }

使用懶載入避免了無用的效能開銷。
需要注意的是直接呼叫R.id::class獲取的是KClass物件,它代表Kotlin中反射的入口,要獲取Java的Class物件,需要其.java屬性,例如:

        {
            val c = R.id::class
            val fields = c.java.fields
            val r = fields.find { it.getInt(c) == view.id }?.name?:"Not found"
            r
        }

以上兩種方式代表了通過物件和類名訪問Java原有反射API的方式。

Kotlin中的KClass反射

Kotlin是函數語言程式設計語言,它有一些獨有的特性,例如,在Kotlin中的Property往往對應了Java中的Field以及對應的getter/setter,
而函式本身也具有型別,也可以作為變數儲存。
要使用Kotlin的反射Api,需要獲取對應的KClass物件,可以通過以下方式:
1.類名::class

val clazz = Country::class

2.物件.javaclass.kotlin

val clazz = country4.javaClass.kotlin

KClass是一個泛型介面,它的定義如下:

public interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {
    //返回類的名字
    public val simpleName: String?

    //返回類的全包名
    public val qualifiedName: String?

     //返回這個類可訪問的所有函式和屬性,包括繼承自基類的,但是不包括構造器
    override val members: Collection<KCallable<*>>

    //返回這個類的所有構造器
    public val constructors: Collection<KFunction<T>>

     //返回這個類中定義的其他類,包括內部類(inner class宣告的)和巢狀類(class宣告的)
    public val nestedClasses: Collection<KClass<*>>

     //如果這個類宣告為object,則返回其例項,否則返回null
    public val objectInstance: T?

    // 判斷一個物件是否為此類的例項
    // 和 物件 is 類名 作用一樣,如:  country3 is Country
    @SinceKotlin("1.1")
    public fun isInstance(value: Any?): Boolean

     // 返回這個類的泛型列表
    @SinceKotlin("1.1")
    public val typeParameters: List<KTypeParameter>

     //以列表的方式依次顯示其直接基類
    @SinceKotlin("1.1")
    public val supertypes: List<KType>

     // 返回這個類的可見性
    @SinceKotlin("1.1")
    public val visibility: KVisibility?

    // 這個類是否為final類(PS:在Kotlin中,類預設是final的,除非這個類宣告為open或者abstract)
    @SinceKotlin("1.1")
    public val isFinal: Boolean

    // 這個類是否是open的(abstract類也是open的),表示這個類可以被繼承
    @SinceKotlin("1.1")
    public val isOpen: Boolean

    //是否為抽象類
    @SinceKotlin("1.1")
    public val isAbstract: Boolean

    //判斷是否為密封類
    /* 密封類:用sealed修飾,其子類只能在其內部定義 */
    @SinceKotlin("1.1")
    public val isSealed: Boolean

     // 判斷類是否為data類
    @SinceKotlin("1.1")
    public val isData: Boolean

     // 判斷類是否為內部類(巢狀類為nest,不算)
    @SinceKotlin("1.1")
    public val isInner: Boolean

    //判斷這個類是否為companion object
    @SinceKotlin("1.1")
    public val isCompanion: Boolean

}

除此之外,KClass還有一些很有用的擴充套件函式/屬性,例如:

//返回其所有的基類
val KClass<*>.allSuperclasses: Collection<KClass<*>>
//返回其companionObject
val KClass<*>.companionObject: KClass<*>?
//返回其宣告的所有函式
val KClass<*>.declaredFunctions: Collection<KFunction<*>>
//返回其擴充套件函式和屬性
val KClass<*>.declaredMemberExtensionFunctions: Collection<KFunction<*>>
val <T : Any> KClass<T>.declaredMemberExtensionProperties: Collection<KProperty2<T, *, *>>
//返回其自身宣告的函式和屬性
val KClass<*>.declaredMemberFunctions: Collection<KFunction<*>>
val <T : Any> KClass<T>.declaredMemberProperties: Collection<KProperty1<T, *>>

其實還有很多,這裡就不一一列舉了,它們其實都可以通過基本Api然後進行filter獲得
需要注意的是,在函式作為一等公民以後,函式和屬性具有了共同的介面KCallable,允許你呼叫其call方法來使用函式或者訪問屬性的getter:

class DVT {
    fun test()
    {
        val su = Person("su",24)
        val clazz = su.javaClass.kotlin
        val list = clazz.members
        for(calls in list)
        {
            when(calls.name)
            {
                "name" -> print("name is"+calls.call(su))
                "age" -> print("age is"+calls.call(su))
                "selfDescribe" -> calls.call()
            }
        }
    }
}
data class Person(val name : String,var age : Int)
{
    fun selfDescribe() : String
    {
        return "My name is $name,I am $age years old"
    }
}

需要注意,call這個方法的引數型別是vararg Any?,如果你用錯誤的型別實參(數量不一致或者型別不一致)去呼叫是會報錯的,
為了避免這種情況,你可以用更具體的方式去呼叫這個函式。

class DVT {
    fun test()
    {
        val su = Person("su",24)
        val clazz = su.javaClass.kotlin
        val function1 = Person::selfDescribe
        val function2 = Person::grow
        function1.invoke(su)
        function2.invoke(su,1)
    }
}
data class Person(val name : String,var age : Int)
{
    fun selfDescribe() : String
    {
        return "My name is $name,I am $age years old"
    }
    fun grow(a : Int) : Int
    {
        age+=a
        return age
    }
}

function1的型別是KFunction0<String>,function2的型別是KFunction1<Int,Int>,像KFunctionN這樣的介面代表了不同數量引數的引數,
它們都繼承了KFunction並添加了一個invoke成員,它擁有數量剛好的引數,包含引數和返回引數
這種型別稱為編譯器生成的型別,你不能找到它們的宣告,你可以使用任意數量引數的函式介面(而不是先宣告一萬個不同引數數量的介面)
對於call函式,它是對於所有型別通用的手段,但是不保證安全性。
你也可以反射呼叫屬性的getter和setter:

        val ageP = Person::age
        //通過setter-call呼叫(不安全)
        ageP.setter.call(24)
        //通過set()呼叫(安全)
        ageP.set(su,24)
        //通過getter-call呼叫(不安全)
        ageP.getter.call()
        //通過get呼叫(安全)
        ageP.get(su)

所有屬性都是KProperty1的例項,它是一個泛型類KProperty1<R,T>,其中R為接收者型別(文中的Person類),T為屬性型別(文中為Int),
這樣就保證了你對正確型別的接收者呼叫其方法。
其子類KMutableProperty代表var屬性

相容問題

雖然Kotlin號稱完全相容Java,但是從註解和反射的概念來看,似乎它並不代表一切Java的工具庫都可以應用到Kotlin中。
例如通過對程式碼註解自動生成模板程式碼的庫(APT),生成的程式碼均為 Java 程式碼而非 kt程式碼。對於一些作用於java檔案到class檔案轉換過程中的庫(AspectJ),實際上是很無力的,它不能作用於kt檔案,大部分基於這個過程的AOP框架都不能正常工作。唯一能正確作用的是.class到.dex轉換中的庫(熱修復,基於Javaassist)。因此,如果要引入前兩種第三方庫,需要確認它們是否支援Kotlin語言。
對於程式碼註解自動生成模板程式碼的庫(Dagger,ButterKnife,DBFlow這些常見的APT庫),可以嘗試Kotlin Annotation processing tool,簡稱kapt,但是並不保證能完全正常工作