秒懂Java反射

ShuSheng007發表於2018-08-19

版權申明】非商業目的註明出處可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/81809999
出自:shusheng007


#前言
前段時間看 Retrofit2原始碼 的時候,發現其大量使用了反技術,在此框架中使用反射技術來獲取方法以及其引數的註解。雖說反射技術在我們日常的開發當使用不是很頻繁,但是其在構建框架則會大放異彩。反射技術應該也算是Java進階的知識了,對有追求的Java程式設計師來說是必須要掌握的一項技能。

#概述
什麼是反射?解決什麼問題?具體如何使用?其是什麼原理?有什麼弊端?
#什麼是反射

In computer science, reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.
在計算機領域,反射是一種計算機程式在程式執行時檢查,自省,改變其結構和行為的能力。(自省的意思是:可以在執行時檢查一個類的型別,屬性等資訊)

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

#解決什麼問題
一項技術能解決什麼問題,關鍵是要從其具有的特性出發去探尋。我們知道,反射的特性就是具有動態,那麼在任何需要在執行時對型別做操作的場景下都可以考慮反射。

1.可以根據配置檔案來動態生成相應的型別物件。

例如我們現在要在mysqloracle資料庫直接切換,如何不使用反射我們就需要在程式碼中預先寫好所有的分支邏輯,而通過反射我們就可以使用配置檔案,然後動態生成相應的類,這樣就大大增強了可擴充套件性。例如現在產品經理要求接入SqlServer資料庫,如果不使用反射就得改程式碼了,然後重新編譯打包。

2.生成動態代理

3.可以突破一些sdkAPI介面限制。

例如在日常開發當中想要訪問一個類的私有欄位,私有方法等。
等等場景。

#什麼原理
Java程式是執行在JVM中的,使得我們可以拿到執行時的型別資訊,所以我們就可以對這些型別資訊做動態的處理了。要想比較深入的理解Java反射的原理,首先要對Java虛擬機器機制以及類載入機制有一定的理解。

##Java執行機制
眾所周知Java程式是執行在JVM上的,我們簡單的可以把JVM 理解成一個執行在作業系統上的單獨的程式。我們寫好一個java類 HelloWorld.java,首先要使用Java編譯器將其編譯為ByteCode(位元組碼) 存放在.class格式的檔案中。然後Java虛擬機器通過類載入器.class檔案載入到虛擬機器中,為HelloWorld 類生成一個對應的Class 物件,接著就可以執行相關的操作了。

Java跨平臺能力就是通過這個機制實現的,因為所有的java程式都是跑在Java虛擬機器上的,所以我們只需要按照《Java虛擬機器規範》開發相應平臺的虛擬機器就好了,那樣一個java程式開發出來就可以不加修改和編輯直接執行在不同的平臺上了。例如Java虛擬機器HotSpot同時存在Windows,Linux,Mac 三個平臺的版本,那麼我們這個helloword java程式就實現了跨平臺。

##類載入機制
其實將反射的話,理解Java執行機制就足夠了,此處稍微深入一點,畢竟類的載入是反射的前提。
###類的載入
類從被載入到虛擬機器記憶體開始,到解除安裝出記憶體為止,宣告週期包括:載入——連線——初始化——使用——解除安裝。其中連線又包含驗證,準備,解析三個步驟。

如下圖所示,圖片來源於周志明的《深入理解Java虛擬機器》一書
這裡寫圖片描述
1: 載入
>通過一個類的全限定名來獲取定義此類的二進位制位元組流。
將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

上面是《Java虛擬機器規範》中對載入的規定,可以簡單理解為使用類載入器將class檔案載入虛擬機器記憶體。

2:驗證
驗證的目的是為了確保 class 檔案中的位元組流包含的資訊符合當前虛擬機器的要求,而且不會危害虛擬機器自身的安全。不同的虛擬機器對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:檔案格式的驗證、後設資料的驗證、位元組碼驗證和符號引用驗證。

檔案格式驗證:要驗證的位元組流是否符合Class檔案格式的規範。例如Class檔案格式要求以魔數0xCAFEBABE開頭,那麼如果我們的位元組流不符合這個規定就會驗證失敗。
後設資料的驗證:對類的後設資料資訊進行語義校驗,保證不存在不符合Java語言規範的後設資料資訊。例如類A是使用final修飾的類,那麼我們知道這個類是不允許繼承的,但是如果我們的位元組流中包含了繼承至A類的B類,那麼此步驟就會驗證失敗。
位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。例如我們將String型別的資料賦值給int等操作。
符號引用驗證:要明白符號引用驗證首先需要明白什麼是符號,我們這裡簡單的描述一下,例如你在class檔案中使用java.lang.String這樣一個符號代表一個你要引用的類,那麼虛擬機器就需要在解析的時候將這個符號java.lang.String解析成直接引用,例如解析出這個類在虛擬機器記憶體中的位置等。

3.:準備
準備階段是正式為類變數分配記憶體並設定類變數的初始值階段,這些變數所在使用的記憶體都將在方法區中進行分配。即為使用static修飾的類變數分配記憶體及初始化為預設值。

4: 解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。由於Java是在類載入中動態連線的,所以class檔案中不
儲存方法及欄位的記憶體佈局資訊,因此這些欄位和方法需要通過解析這一步才能獲得真正的記憶體直接入口地址。

5:初始化
類初始化是類載入過程的最後一個階段,到初始化階段,才真正開始執行類中的 Java 程式程式碼。

###類載入器
從開發者的角度來看,類載入器分為4
這裡寫圖片描述

  1. 啟動類載入器(Bootstrap ClassLoader):負責載入Java的核心類庫。即%JAVA_HOME%/lib路徑下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫。sunHotSpot虛擬機器的啟動類載入器由 C++ 實現,不是 ClassLoader 子類,無法被 Java 程式直接引用的。
  2. 擴充套件類載入器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實現,負責載入Java平臺中擴充套件功能的一些jar包,%JAVA_HOME%/jre/lib/ext或由 java.ext.dirs指定目錄下的 jar 包。開發者可以直接使用擴充套件類載入器。
  3. 應用程式類載入器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 來實現,負責載入使用者類路徑(ClassPath ) 中指定的類庫,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

#如何使用
反射的一般使用較為簡單,首先獲得Class物件,然後根據要獲取的操作資訊,例如獲取類的建構函式,方法,屬性等呼叫相應的方法即可。

其中Class代表物件;Constructor 代表建構函式;Method 代表方法;Field代表欄位。

現在假設我們有如下一個類

package top.ss7.reflect;

/**
 * Created by shusheng007 on 2018/8/19.
 */
@ProGender(gender = "女")
public class Programmer {
    private String name;
    private String proLang;

    public String pmName;

    public Programmer(){
        System.out.println("public 無引數構造器");
    }

    private Programmer(int num){
        System.out.println("private 一個Int型別引數構造器");
    }

    public Programmer(String proLang){
        this.name="ss007";
        this.proLang=proLang;
        System.out.println("public 一個String型別引數構造器");
    }

    public Programmer(String name,String proLang){
        this.name=name;
        this.proLang=proLang;
        System.out.println("public 兩個String型別引數構造器");
    }


    public void work(){
        System.out.println(String.format("%s:在使用 %s 開發語言工作。", name,proLang));
    }

    private void workForModifiedDemand(String pName){
        System.out.println( pName+" fuck you PM: "+pmName);
    }
}

註解類ProGender

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProGender {
    String gender() default "男";
}

##獲取Class物件
有三種方式獲取一個類的Class物件:

  1. 呼叫某個類的class屬,例如 Programmer.class即可返回Programmer 類的Class物件;
  2. 使用Class類的靜態方法 Class.forName("類全限定名"),這種方式使用還是比較多的,可以通過一個字串來生成相應的物件。
  3. 呼叫某個物件的getClass()方法,該方法是java.lang,Object裡面的方法,所以所有的Java物件都可以呼叫。

如果三種都可以使用的情況下,優先使用第一種方式。一旦多的Class物件,就可以呼叫其方法來獲取此物件的真是資訊了。下面程式碼演示了這三種方法。

Class<Programmer> pClass=Programmer.class;

try {
    Class<?> pClass2=Class.forName("top.ss7.reflect.Programmer");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}  

Programmer p=new Programmer();
Class<?> pClass3=p.getClass();

##獲取建構函式
我們知道一個類可以有很多建構函式,分為無引數構造器,與有引數構造器。

獲取無引數構造器:

public T newInstance()

獲取有引數public構造器:

public Constructor<T> getConstructor(Class<?>... parameterTypes)

獲取有引數pivate構造器:

public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

如下例項所示

    public static void main(String[] args) {
        Class<Programmer> pClass=Programmer.class;
        try {
            //獲取無引數公有建構函式
            Programmer programmer1=pClass.newInstance();
            
            //獲取含有一個String型別引數的公有建構函式
            Constructor pCon=pClass.getConstructor(String.class);
            Programmer programmer2= (Programmer) pCon.newInstance("Java");

            //獲取含有一個int型別引數的私有建構函式
            Constructor pCon2=pClass.getDeclaredConstructor(int.class);
            pCon2.setAccessible(true);
            Programmer programmer3= (Programmer) pCon2.newInstance(10);

            //獲取含有兩個String型別的公有建構函式
            Constructor pCon3=pClass.getConstructor(String.class,String.class);
            Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");
        } 
        ...
    }

執行結果為:

public 無引數構造器
public 一個String型別引數構造器
private 一個Int型別引數構造器
public 兩個String型別引數構造器

關於獲取建構函式的方法還有幾個,可以參考Java Api
##獲取方法
方法的獲取與建構函式的獲取非常相似,只是可以使用方法名稱來增強指向性。

獲取public方法:

public Method getMethod(String name, Class<?>... parameterTypes)

獲取private方法:

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)

第一個引數為方法名稱,第二個引數為方法引數型別的陣列。

我們的Programmer類有兩個方法,一個是公有的一個是私有的,呼叫如下:

 //獲取含有兩個String型別的公有建構函式
 Constructor pCon3=pClass.getConstructor(String.class,String.class);
 Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");

 //呼叫公有方法
 Method m1=pClass.getMethod("work");
 m1.invoke(programmer4);

//呼叫私有方法
 Method m2=pClass.getDeclaredMethod("workForModifiedDemand",String.class);
 m2.setAccessible(true);
 m2.invoke(programmer4,"王二狗");

執行結果為:

王二狗:在使用 C++ 開發語言工作。
王二狗 fuck you PM: null

可以看到王二狗要fuck的那個產品經理為null,那是因為我們還沒有為那個欄位賦值,接下來就看一下如何通過反射操作Field

##獲取屬性
屬性的獲取就更簡單了,直接使用屬性名稱獲取。

獲取public屬性:

public Method getMethod(String name, Class<?>... parameterTypes)

獲取private屬性:

 public Field getDeclaredField(String name)

方法的引數為屬性名,現在我們訪問Programmer類的兩個屬性,並對其中一個賦值

//獲取含有兩個String型別的公有建構函式
Constructor pCon3=pClass.getConstructor(String.class,String.class);
Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");
//設定公有屬性的值
Field f1=pClass.getField("pmName");
f1.set(programmer4,"牛翠花");

//訪問私有屬性的值
Field f2=pClass.getDeclaredField("name");
f2.setAccessible(true);
System.out.println("程式設計師名稱被設定為:"+f2.get(programmer4));

輸出結果為:

程式設計師名稱被設定為:王二狗
王二狗 fuck you PM: 牛翠花

##獲取註解
我通過反射也可以獲取到各種註解(Annotation),例如此例中我們簡單的獲取一下Programmer類上面的註解資訊

ProGender pAno= pClass.getAnnotation(ProGender.class);
System.out.println("程式設計師性別為:"+pAno.gender());

其實通過反射還可以得到方法引數的資訊,有興趣的同學可以研究。

反射的優缺點

優點

1:可以在執行時獲得一個編譯時還不存在的類的資訊。

缺點

  1. 編譯器將無法在編譯期間為我們做型別檢查的工作,就是說型別錯誤不能在編譯時候被發現了
  2. 使用反射時候的程式碼真的是又亂又難寫,而且還容易出錯
  3. 慢!至於為什麼慢,大家提及最多的就是不使用反射時
    a:編譯器會優化物件例項化的過程,而使用反射則完全不優化,那麼例項化物件這塊就會有比較大的差距。
    b:查詢檢查等操作上的差距

發射在日常開發中的開啟方式

在我們日常開發中遇到的大部分情況是我們無法在編譯器獲得一個類物件,需要根據執行時候的配置來生成相應的物件,例如通過一個類的全限定類名字串來生成類的物件。
比較好的做法就是讓這些類實現一個編譯時存在的介面,或者抽象類,然後執行時通過反射獲得其例項物件,然後賦值給這些以介面申明的變數中,然後以介面來編寫相關業務程式碼。

總結

關於反射,能不用則不用,一有效能問題,二有複雜度問題。技術無好壞關鍵是適不適合當前使用場景。

范冰冰固然漂亮,但是不一定適合你,什麼?你說不試一試怎麼知道,那這個試錯成本你承受的了嗎?如果你一定要堅持就先拿5千萬出來,我找王婆幫你聯絡。。。,記住是$

希望廣大程式設計師生活愉快,編碼愉快!

最後,求關注,求點贊!有任何疑問可以評論留言,我會盡力回覆的。

相關文章