Java反射,就是這麼簡單

一文帶你完全理解反射

前言

人與人交流要用語言,人與機器人的交互一樣須要語言,從計算機誕生至今,計算機語言經歷了機器語言彙編語言高級語言。在全部的程序設計語言中,只有機器語言可以被計算機直接理解和執行,而其餘程序語言都必須先翻譯成與機器語言,才能和計算機交互。java

靜態語言和動態語言python

對於咱們來講,接觸作多的就是高級語言,包括C、C++、Java、python、JavaScript等。這些高級語言能夠大概分爲兩大類,即動態語言靜態語言編程

  • 靜態語言

通俗來說,若是在編譯時就知道變量的類型,該可認爲該語言是靜態的,如咱們所熟知的Java、C、C++等,它們在寫代碼時必須指定每一個變量的類型。數組

  • 動態語言

動態語言通常指的是腳本語言,如python、JavaScript等,這類語在編寫代碼是沒必要指定類型安全

  • 動態語言VS靜態語言

從直觀上看,靜態語言在代碼編譯時須要指定變量類型;而動態語言則是在運行期間纔會檢查變量類型。因此,針對動態語言來講,咱們能夠在運行時改變其結構,即運行時的代碼能夠根據某些條件改變自身的結構。markdown

按照劃分,Java是屬於靜態語言的,可是因爲Java提供了反射機制,使得Java成爲了一種準動態語言,利用反射能夠得到相似動態語言的特性,使得編程更加的靈活。數據結構

下面,咱們就認真學習下Java反射是什麼怎麼使用爲什這麼使用多線程

一、Java反射機制的基本概述

  • 什麼是反射?

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

看到官方解釋,你們也許會有點懵,不要着急,咱們想一下在平常的開發過程當中,咱們常常會遇到某個類中的成員變量、方法是私有的,這些成員、方法是不對外開發的,可是咱們能夠經過Java的反射機制在運行期間動態的獲取。因此,咱們對Java反射能夠從新理解以下:反射就是程序在運行時,能夠根據類的全限定名稱,動態地加載該類,建立對象,並能夠調用該對象中地任意屬性方法測試

那麼,問題來了,爲何要學習反射呢?

咱們想象這樣一個場景,當咱們在程序中須要一些功能的時候,咱們通常採用的方式就是先new一個對象,而後從對象中獲取咱們所需功能的方法,可是咱們有沒有想過,若是一個咱們的程序支持插件,可是咱們並不知道這個插件都有哪些類,這種狀況下該怎麼辦呢?還好反射能夠解決這個問題,使用反射能夠在程序運行期間從配置文件中讀取類名,而後動態的獲取對象類型的信息。因此,反射的最大好處就是在運行期間動態的構建組件,使得代碼更具備靈活性和通用性。

正常方式:①引入須要的「包類」名稱②經過new實例化③得到實例化對象

反射方式:①實例化對象②getClass方法③獲得完整的「包類」名稱

2 、理解Class類並獲取Class實例

既然咱們要使用反射建立對象,那麼咱們是如何建立Class呢?針對不一樣的實例對象反射出的對象是不是同一個呢?

獲取Class類三種方式

  • 已知一個類的全限定類名,可經過Class類的靜態方法forName獲取
Class c=Class.forName("java.Lang.String")
複製代碼
  • 已知具體的類,能夠經過該類的class屬性獲取
Class c=Person.class;
複製代碼
  • 已知某個類的實例,調用該實例的getClass()方法獲取Class對象
Class c=person.getClass();
複製代碼

實例代碼(以第一種方法爲例)

  • 建立Person類
class People{
    private int id;
    private int age;
    private String name;
....
}

複製代碼
  • 測試類1
public class TestReflrction01 {
    public static void main(String[] args) throws ClassNotFoundException {
        //經過反射獲取類的Class對象
        Class c1=Class.forName("reflection.People");
        System.out.println(c1);
    }
}
複製代碼

測試類的輸出爲:class reflection.People

那麼,問題又來了,對於不一樣的實例對象獲取的Class類時否是同一個呢?咱們採用這樣的方式,咱們獲取不一樣實例對象獲取的Class的hashCode,若是hashCode相同,則可證實他們是同一個Class

  • 測試類2
public class TestReflrction01 {
    public static void main(String[] args) throws ClassNotFoundException {
        //經過反射獲取類的Class對象
        Class c1=Class.forName("reflection.People");
        System.out.println(c1);
        //內存中只存在一個類的Class對象
        //一個類被加載後,類的整個結構都會封裝在Class對象中
        Class c2=Class.forName("reflection.People");
        Class c3=Class.forName("reflection.People");
        Class c4=Class.forName("reflection.People");

        System.out.println(c2.hashCode());
        System.out.println(c3.hashCode());
        System.out.println(c4.hashCode());
    }
}
複製代碼

測試類的輸出以下:

class reflection.People 460141958 460141958 460141958 複製代碼

所以,咱們能夠判定,同一個類的不一樣的實例對象反射出class對象是同一個,是惟一的。

三、類的加載過程以及反射建立對象時的內存分析

3.1類的加載過程分析

上面咱們學習瞭如何建立Class類,可是咱們確定會有這樣的疑惑,爲何能夠動態建立Class類呢,它的原理是什麼呢?要想了解它的原理,咱們必須先了解下JVM內存

咱們先看這樣一張流程圖

這張圖詳細的描述了咱們編寫的Java文件的執行流程,由於這裏涉及了不少JVM的知識點,感興趣的同窗能夠先看看我之前的一篇文章一文入門JVM虛擬機,後面還會繼續補充相關知識點,在這裏,咱們主要分析類加載過程

咱們都瞭解java程序都是放在虛擬機上執行的,Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗轉換解析初始化,最終造成能夠被虛擬機直接使用的Java類型。

其中驗證準備解析統稱爲鏈接,下面咱們詳細分析下類的加載過程

  • 加載
  • 經過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區運行時數據結構
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲這個方法區這個類的各類數據的訪問入口
  • 連接:將Java類的二進制代碼合併到JVM的運行態之中的過程
  • 驗證:確保加載的類信息符合JVM規範,沒有安全方面的問題
  • 準備:正式爲了類變量(static)分配內存並設置類變量默認初始值階段,這些內存都將在方法區中分配
  • 解析:虛擬機常量池內的符號引用(常量名)替換爲直接引用(地址)的過程

Note:對於常量池中的符號引用解析,要結合具體的實際狀況自行判斷,究竟是在類加載器加載時就對常量池中的符號引用解析,仍是等到一個符號引用將要被使用前採起解析它。

  • 初始化

初始化階段是類加載過程的最後一個階段,在這個階段時,Java虛擬機才真正開始執行類中編寫的Java程序代碼,將主導權移交給應用程序。初始化步驟以下:

  • 執行類構造器()方法的過程。類構造器()方法是由編譯期自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併產生。(類構造器是構造類信息的,不是構造該類對象的構造器)
  • 當初始化一個類的時候,若是發現其父類尚未進行初始化,則須要先觸發其父類的初始化
  • 虛擬機會保證一個類的()方法在多線程環境下被正確加鎖和同步。

這時,咱們可能會有疑惑,何時會發生類的初始化呢?事實上,只要當類主動引用時纔會發生初始化。

  • 類的主動引用
  • 當虛擬機啓動,先初始化main方法所在的類
  • new一個類的對象
  • 調用類的靜態成員(除了final常量)和靜態方法
  • 使用java.lang.reflect包的方法對類進行反射調用
  • 當初始化一個類,若是其父類沒有被初始化,則先會初始化它的父類

那麼,是否是能夠理解爲,類的被動引用就不會發生初始化了,是的,下面列出的這幾種狀況就不會發生類的初始化

  • 類的被動引用
  • 當訪問一個靜態域時,只有真正聲明這個域的類纔會被初始化。如:當經過子類引用父類的靜態變量,不會致使子類的初始化
  • 經過數組定義類引用,不會觸發此類的初始化
  • 引用常量不會觸發此類的初始化(常量在連接階段就存入調用類的常量池中)

3.2 使用反射建立對象的內存分析

上面咱們詳細分析了Java的內存分佈和類的加載流程,此時,咱們編寫的代碼已經處於在運行期了,咱們知道利用反射能夠在運行期動態的建立對象,那麼它的工做機制究竟是什麼樣的呢?在下面的文章中,咱們詳細分析

上圖是咱們類加載過程結束後的內存分佈,每一個類都在堆中建立了表明本身本類的Class類。記住,這個Class類是用於建立Class對象的,咱們繼續向下分析。

當咱們在棧中new A時,它首先會找到堆中的Class類,由於Class類是訪問方法區類A中各類數據的訪問入口。而後將相應的類信息帶到堆中完成實例化。

這也就不難理解爲爲何能夠反射能夠在運行時期動態的獲取的對象。在下面的文章中,咱們將詳細講解如何使用反射,即怎樣利用反射建立運行時類對象,怎麼獲取運行時類的完整結構,如何調用運行時類的指定結構。

3.3反射相關API和提供的主要功能概述

反射相關的API

  • java.lang.Class:表明一個類
  • java.lang.reflect.Method:表明類的方法
  • java.lang.reflect.Field:表明類成員變量
  • java.lang.reflect.Constructor:表明類的構造器

反射機制提供的主要功能

  • 在運行時判斷任意一個對象所屬的類
  • 在運行時構造任意一個類的對象
  • 在運行時判斷任意一個類所具備的成員變量和方法
  • 在運行時獲取泛型的信息
  • 在運行時調用任意一個對象的成員變量和方法
  • 在運行時處理註解
  • 生成動態代理

四、建立運行時類對象

在程序運行期間,Java運行時系統始終爲全部對象維護一個被稱爲運行時的類型標識。這個信息跟蹤着每一個對象所屬的類。虛擬機利用運行時類型信息選擇相應的方法執行。Java中提供了專門的類訪問這些信息,保存這些信息的類稱爲Class。這個名稱很容易讓人產生混淆,由於在Object類中有一個方法用獲取Class實例,此方法能夠被全部的子類繼承

public final Class getClass
複製代碼

4.1獲取Class對象的三種方式

在Java API中,提供了獲取Class類對象的三種方式

  • 使用Class.forName靜態方法

使用這種的方法的前提是要知道類的全路徑名

//方式一:經過類的全類名獲取,可經過Class類的靜態方法forName()獲取,可能拋出ClassNotFoundException
       Class c1= Class.forName("reflection.People");
複製代碼
  • 使用類的.class方法

該方法適用於在編譯前就已經明確要操做的Class

//方式二:若一致具體的類,經過類的class屬性獲取
       Class c3=People.class;
複製代碼
  • 使用類對象的getClass()方法

該方法適用於有對象實例的狀況下

//方式二:調用該實例的getClass()獲取
       People people=new People();
複製代碼
  • 代碼驗證
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException {

        //方式一:經過類的全類名獲取,可經過Class類的靜態方法forName()獲取,可能拋出ClassNotFoundException
       Class c1= Class.forName("reflection.People");
       System.out.println(c1);

       //方式二:調用該實例的getClass()獲取
       People people=new People();
       Class c2=people.getClass();
       System.out.println(c2);

        //方式三:若一致具體的類,經過類的class屬性獲取
       Class c3=People.class;
       System.out.println(c3);
    }
}
複製代碼

總結

實際上,對於每一個類而言,JRE都爲其保留一個不變的Class類型的對象。一個Class對象包含特定某個結構的有關信息。Class總結以下:

  • Class自己也是一個類
  • Class對象只能由系統創建對象
  • 一個加載的類在JVM中只會有一個Class實例
  • 一個Class對象對應的是一個加載到JVM中的一個.Class文件
  • 每一個類的實例都會記得本身是由哪一個Class實例所生成的
  • 經過Class能夠完整地獲得一個類中的全部被加載的結構
  • Class類時是Reflection的根源,針對任何你想動態加載、運行的類,惟有先得到相應的Class對象

五、獲取運行時類的完整結構

在上面的文章中,咱們講解了如何使用反射機制來建立Class類對象,當有了實際的對象後,咱們能夠作哪些事情呢?反射機制爲咱們提供了哪些具體的操做方法呢?

java.lang.reflect包中有三個類FieldMethodConstructor分別用於描述類的屬性方法構造器。這三個類都有一個叫作getName的方法,

Class類中的getFieldsgetMethodsgetConstructord方法將分別返回類提供的public(屬性、方法和構造器的數組),其中也包括了父類的public成員。

Class類中的getDeclaredFieldsgetDeclaredMethodsgetDeclaredConstructors方法將分別返回類中聲明的(所有)屬性、方法和構造器,其中包括了私有成員和受保護成員,但不包括父類的成員

5.1獲取運行時類的屬性

  • 方法
Field[] getFields()
Filed[] getDeclaredFields()
複製代碼

getFileds方法將返回一個包含Field對象的數組,這些對象記錄這個類或其父類的public屬性;getDeclaredFileds也將返回一個包含Field對象的數組,這些對象記錄這個類的所有屬性。

Note:若是類中沒有定義屬性,或者Class對象描述的是基本類型或者數組類型,這些方法將返回一個長度爲0的數組

  • 代碼實踐
public class TestReflection04 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
        Class c1=Class.forName("reflection.People");

        //得到類的名字
        System.out.println(c1.getName());//包名+類名
        System.out.println(c1.getSimpleName());//類名

        //得到類的屬性
        Field[] fields=c1.getFields(); //只能找到public
        fields=c1.getDeclaredFields();  //能夠找到所有的屬性
        for (Field field:fields){
            System.out.println(field);
        }

        //得到指定屬性的值
        Field name=c1.getDeclaredField("name");
        System.out.println(name);

    }
}

複製代碼

5.2得到運行時類的方法

  • 方法
Method[] = new getMethods[]
Method[] =new getDeclaredMethods[]
複製代碼

getMethods方法將返回一個包含Method對象的數組,這些對象記錄這個類或其父類的public方法;getDeclaredMethods[]也將返回一個包含Method對象的數組,這些對象記錄這個類或接口的所有方法。

  • 代碼實踐
/** * 獲取類的運行時結構 * 包括方法、屬性、構造器 */
public class TestReflection04 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
        Class c1=Class.forName("reflection.People");

   
       //得到類的方法
        Method[] methods=c1.getMethods();//得到本類及其父類的所有的public方法
        for(Method method:methods){
            System.out.println(method);
        }
        //得到本類的全部方法
        methods=c1.getDeclaredMethods();
        for(Method method:methods){
            System.out.println(method);
        }

        //得到指定的方法
        Method getName=c1.getMethod("getName",null);
        System.out.println(getName);
    }
}
複製代碼

5.3建立運行時類的構造器

  • 方法
Constructor[] getConstructors()
Constructor[] getDeclaredConstructors()
複製代碼

getConstructors方法將返回一個包含Constructor對象的數組,這些對象記錄這個類或其父類的public公有構造器;getDeclaredConstructors也將返回一個包含Constructor對象的數組,這些對象記錄這個類的全部構造器。

  • 代碼實踐
public class TestReflection04 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
        Class c1=Class.forName("reflection.People");
        Constructor[] constructors=c1.getConstructors();
        for (Constructor constructor:constructors){
            System.out.println(constructor);
        }
        constructors=c1.getDeclaredConstructors();
        for (Constructor constructor:constructors){
            System.out.println(constructor);
        }
        //得到指定的構造器
        Constructor declareConstructor=c1.getDeclaredConstructor(int.class,int.class,String.class);
        System.out.println(declareConstructor);
    }
}

複製代碼

六、動態建立類的對象

上面的文章中,咱們講解了如何獲取類的運行時結構,若是咱們要使用,必須建立類的對象

建立類的對象:調用Class對象newInstance()方法

類必須有一個無參構造器,由於當操做時,若沒有明確調用類中的構造器,則會調用無參構造器,若要實例化對象,須要使用構造器

類的構造器訪問權限須要足夠

  • 經過Class類的getDeclaredConstructor(Class.... parameTypes)得到本類的指定形參類型的構造器
  • 向構造器的形參中傳遞一個對象數組中去,裏面包含了構造器中所需的各個參數
  • 經過Constructor實例化對象

測試代碼

public class TestRelection05 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class c1=Class.forName("reflection.People");
        //構造一個對象,本質上調用了無參構造器
        People people=(People) c1.newInstance();
        System.out.println(people);

        //經過構造器建立對象
        Constructor constructor=c1.getDeclaredConstructor(int.class,int.class,String.class);
        People people2=(People) constructor.newInstance(23,25,"Simon");
        System.out.println(people2);
        //經過反射調用普通方法
        People people3=(People) c1.newInstance();
        Method setName=c1.getDeclaredMethod("setName", String.class);
        //invoke:激活的意思(對象,"方法的值")
        setName.invoke(people3,"Simon Lang");
        System.out.println(people3.getName());

        //經過反射調用屬性
        People people4=(People) c1.newInstance();
        Field name=c1.getDeclaredField("name");
        //不能直接操做私有屬性,咱們須要關閉程序的安全監測,屬性或者方法的setAccessible(true)
        name.setAccessible(true);
        name.set(people4,"Simon snow");
        System.out.println(people4.getName());
    }
}

複製代碼

setAccessible

  • Method和Field、Constructor對象都setAccessible()方法
  • setAccessible做用是啓動和禁用訪問安全檢查的開關
  • 參數值爲true則指示反射的對象在使用時用該取消Java語言的訪問檢查
  • 使得本來沒法訪問的私有成員也能夠訪問
  • 參數值爲false則指示反射的對象應該實施Java語言的訪問檢查

山腰太擁擠了,我想去山頂看看

相關文章
相關標籤/搜索