Java 反射(一)反射簡介、原理和應用場景


一、動態語言和動態語言的比較

動態語言

➢ 是一類在運行時可以改變其結構的語言:例如新的函數、對象、甚至代碼可以被引進,已有的函數可以被刪除或是其他結構上的變化。通俗點說就是在運行時代碼可以根據某些條件改變自身結構
➢ 主要動態語言: Object-CC#JavaScriptPHPPython等。

一個具體例子:

# test.py
a=1
s = "print(a)"
eval(s)
======運行結果======
1

可以看出,在python這種動態語言中,一個字符串拼接成的代碼可以被直接運行,這在c++這些靜態語言中是完全無法做到的,因為運行的機器代碼在編譯完成那一刻就不再改變了。

靜態語言

➢ 與動態語言相對應的,運行時結構不可變的語言就是靜態語言。如JavaCC++.
➢ Java不是動態語言,但Java可以稱之為“准動態語言”.。即Java有一定的動態性,我們可以利用反射機制獲得類似動態語言的特性。Java的動態性讓編程的時候更加靈活!

二、反射

簡介

Reflection (反射)是Java視為動態語言的關鍵,反射機制允許程序在執行期借助於Reflection API取得任何類的內部信息,並能直接操作任意對象的內部屬性及方法

得到任何類的內部信息可以:
➢ 在運行時判斷任意一個對象所屬的類
➢ 在運行時構造任意一個類的對象
➢ 在運行時獲取任意一個類所具有的成員變量和方法
➢ 在運行時獲取泛型信息
➢ 在運行時調用任意一個對象的成員變量和方法
➢ 在運行時處理注解
➢ 生成動態代理

因此利用反射機制,在運行時我們就能通過一個類名(字符串)來調用一個類的構造函數生成實例對象,甚至可以拼接出類的成員方法使用反射進行相應的調用等等。

反射的常見使用

1. 代碼編輯器

反射最常見的使用莫過於我們編寫java程序需要依賴的代碼編輯器了,這里用IDEA為例,當我們輸入‘對象.’時,即會顯示出對象的各個成員方法的提示框,如:
在這里插入圖片描述
代碼編輯器正是通過反射機制獲取到對象所在類的成員方法,進而以UI的方式展示在用戶的面前。

2. Spring等框架的IoC容器

此外使用Spring框架的IoC容器時,若我們使用xml文件設置JavaBean,就會發現它需要我們填入全類名,然后會在運行時在容器中保存該對象,其背后的原理就是Spring框架從xml文件中讀取到類名字符串,然后通過反射取到對應的類,進而調用該類中的構造函數新建出實例對象保存起來。

Spring中xml配置的bean:

<bean id="hello" class="pojo.Hello"> <--!填入全類名-->
    <property name="str" value="Spring"/>
</bean>

3. 和注解的配合使用

如前面寫的注解文章所說,注解相當於程序的tag,需要一個讀取的工具查看tag中的信息,進而進行不同的操作。而這個讀取的工具就是反射

例如在SpringBoot框架中,我們希望生成一個類的實例對象(單例)並將其放置到IoC容器中,只需要在哪個類打上@Component等注解即可,SpringBoot正是通過反射讀取到xx類中表明了這個注解才會將其放置到IoC容器中進行管理:

@Service <---
public class EmployeesServiceImpl extends ServiceImpl<EmployeesMapper, Employees> implements EmployeesService {
}

原理

java虛擬機在加載完類之后,在堆內存的方法區中就產生了一個Class類型的對象(一個類只有一個Class對象),這個對象就包含了完整的類的結構信息。我們可以通過這個對象看到類的結構。這個對象就像一面鏡子, 透過這個鏡子看到類的結構,所以,我們形象的稱之為:反射

反射優缺點

優點:
➢ 可以實現動態創建對象和編譯,體現出很大的靈活性
缺點:
➢對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什么並且它滿足我們的要求。這類操作總是慢於直接執行相同的操作

調試查看

我們先使用IDE進行調試查看一下Class對象的內部信息:

// 使用全類名獲取到對應類的Class對象
Class c1 = Class.forName("reflection.User");
在這里插入圖片描述

可以看到這個對象中確實包含了許多這個類的內部信息。

tips:

  1. 一個類在內存中只有一個Class對象
  2. 一個類被加載后,類的結構都被封裝在Class對象中

Class類

對象照鏡子后可以得到的信息:某個類的屬性、方法和構造器、某個類到底實現了哪些接口。對於每個類而言,JRE都為其保留一個不變的Class類型的對象。一個Class對象包含了特定某個結構(class/interface/enum/annotation/primitive type/void/[])的有關信息。
➢Class本身也是一個類
➢Class 對象只能由系統建立對象
➢一個加載的類在JVM中只會有一個Class實例
➢一個Class對象對應的是一個加載到JVM中的一個class文件
➢每個類的實例都會記得自己是由哪個Class實例所生成
➢通過Class可以完整地得到一個類中的所有被加載的結構
Class類是Reflection的根源, 針對任何你想動態加載、運行的類,唯有先獲得相應的Class對象

獲取Class實例的方式

1. 通過對象獲得

// 方式一:通過對象獲得
Person person = new Student();
Class c1 = person.getClass();

常用的方式是某個方法接收的參數是一個抽象類或接口,然后通過這種方式可以得到輸入的參數所在的實現類或子類。

2. 通過Class.forname獲得

// 方式二:通過Class.forname獲得
Class c2 = Class.forName("reflection.Student");

輸入一個全類名,可以獲得類(.class文件)對應的Class對象。

3. 通過類名.class獲得

// 方式三:通過類名.class獲得
Class c3 = Student.class;

4.基本內置類型的包裝類用Type屬性獲得

Class c4 = Integer.TYPE;

5. 一個Class實例通過.getSuperclass()獲得父類的Class實例

Class c5 = c1.getSuperclass();

后面還有Class[] getInterfaces()等,這里不再贅述。

哪些類型可以有Class對象

➢class: 外部類,成員(成員內部類,靜態內部類), 局部內部類,匿名內部類。
➢interface: 接口
➢[]:數組
➢enum:枚舉
➢annotation: 注解@interface
➢primitive type: (基本數據類型)
➢void

測試:

public static void main(String[] args) {
    Class c1 = Object.class;        //類
    Class c2 = Comparable.class;    //接口
    Class c3 = String[].class;      //一維數組
    Class c4 = int[][].class;       //二維數組
    Class c5 = Override.class;      //注解
    Class c6 = ElementType.class;   //枚舉
    Class c7 = Integer.class;       //基本數據類型
    Class c8 = void.class;          //void
    Class c9 = Class.class;         //Class
	// 打印...
}
=================運行結果=================
class java.lang.Object
interface java.lang.Comparable
class [Ljava.lang.String;
class [[I
interface java.lang.Override
class java.lang.annotation.ElementType
class java.lang.Integer
void

java內存分析

在這里插入圖片描述

類的加載過程:
在這里插入圖片描述

➢加載:將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區的運行時數據結構,
然后生成一個代表這個類的java.lang.Class對象.

➢鏈接:將Java類的二進制代碼合並到JVM的運行狀態之中的過程。

  • ➢驗證:確保加載的類信息符合JVM規范,沒有安全方面的問題
  • ➢准備:正式為類變量(static) 分配內存並設置類變量默認初始值的階段,這些內存都將在方法區中進行分配。
  • ➢解析:虛擬機常量池內的符號引用(常量名)替換為直接引用(地址)的過程。

➢初始化:

  • ➢執行類構造器< clinit> ()方法的過程。類構造器< clinit> ()方法是由編譯期自動收集類中所有類變量的賦值動作和靜態
    代碼塊中的語句合並產生的。(類構造 器是構造類信息的,不是構造該類對象的構造器)。
  • ➢當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
  • ➢虛擬機會保證一個類的< clinit> ()方法在多線程環境中被正確加鎖和同步。

測試例:

public class Test05 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(A.m);
        /*
        1. 加載到內存,會產生一個類對應的Class對象
        2. 鏈接,鏈接結束后 m=0(初始值)
        3. 初始化
            <clint>(){
                System.out.println("A類靜態代碼塊初始化");
                m=300;
                m = 100;
            }
            m==100
        */
    }
}
class A{
    static {
        System.out.println("A類靜態代碼塊初始化");
        m=300;
    }
    static int m = 100;
    public A(){
        System.out.println("A類的無參構造初始化");
    }
}
========================運行結果========================
A類靜態代碼塊初始化
A類的無參構造初始化
100

下一部分:Java 反射(二)運行時獲取類的信息


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM