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,但是并不保證能完全正常工作

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 前言 人生苦多,快來 Kotlin ,快速學習Kotlin! 什么是Kotlin? Kotlin 是種靜態類型編程...
    任半生囂狂閱讀 26,252評論 9 118
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,743評論 25 708
  • 什么是反射 反射是一種計算機處理方式。有程序可以訪問、檢測和修改它本身狀態或行為的這種能力。能提供封裝程序集、類型...
    黑心石閱讀 5,344評論 1 5
  • 真實案例 “如果藝校是通往完成歌唱家夢想的大門,那么160cm的身高就是這扇大門的敲門磚,可是,我卻連這塊敲門磚都...
    身高管理師閱讀 429評論 0 0
  • 時間在走 年齡在長 懂得的多了 看得的就多了 快樂越來越少 我懷念那些年 未來遙遠的沒有形狀 我們單純的沒有煩惱 ...
    北瑾余生閱讀 126評論 0 0