Java春招面試複習:Java反射的入門到實踐,再到原理

Hi丶ImViper發表於2020-12-18

前言

反射是Java底層框架的靈魂技術,學習反射非常有必要,本文將從入門概念,到實踐,再到原理講解反射,希望對大家有幫助。

反射理解

官方解析

Oracle 官方對反射的解釋是:

Reflection is commonly used by programs which require the ability to examine or
modify the runtime behavior of applications running in the Java virtual machine.
This is a relatively advanced feature and should be used only by developers who
have a strong grasp of the fundamentals of the language. With that caveat in
mind, reflection is a powerful technique and can enable applications to perform
operations which would otherwise be impossible.

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

白話理解

正射

萬物有陰必有陽,有正必有反。既然有反射,就必有“正射”。

那麼正射是什麼呢?

我們在編寫程式碼時,當需要使用到某一個類的時候,都會先了解這個類是做什麼的。然後例項化這個類,接著用例項化好的物件進行操作,這就是正射。

Student student = new Student();
student.doHomework("數學");

反射

反射就是,一開始並不知道我們要初始化的類物件是什麼,自然也無法使用 new 關鍵字來建立物件了。

 Class clazz = Class.forName("reflection.Student");
 Method method = clazz.getMethod("doHomework", String.class);
 Constructor constructor = clazz.getConstructor();
 Object object = constructor.newInstance();
 method.invoke(object, "語文");
​```java

#### 正射與反射對比
以上兩段程式碼,執行效果是一樣的,如圖

![](https://user-gold-cdn.xitu.io/2019/12/13/16efff4ccf899e66?w=1071&h=716&f=png&s=91450)

但是,其實現的過程還是有很大的差別的:

- 第一段程式碼在未執行前就已經知道了要執行的類是```Student```;
- 第二段程式碼則是到整個程式執行的時候,從字串```reflection.Student```,才知道要操作的類是```Student```。


#### 結論

反射就是在執行時才知道要操作的類是什麼,並且可以在執行時獲取類的完整構造,並呼叫對應的方法。

## Class 物件理解
要理解Class物件,我們先來了解一下**RTTI**吧。
**RTTI(Run-Time Type Identification)執行時型別識別**,其作用是在執行時識別一個物件的型別和類的資訊。

Java是如何讓我們在執行時識別物件和類的資訊的?主要有兩種方式:
一種是傳統的**RRTI**,它假定我們在編譯期已知道了所有型別。
另一種是反射機制,它允許我們在執行時發現和使用類的資訊。

**每個類都有一個Class物件**,每當編譯一個新類就產生一個Class物件(更恰當地說,是被儲存在一個同名的.class檔案中)。比如建立一個Student類,那麼,JVM就會建立一個Student對應Class類的Class物件,該Class物件儲存了Student類相關的型別資訊。


![](https://user-gold-cdn.xitu.io/2019/12/21/16f286b0e2fce054?w=1066&h=538&f=png&s=60629)

**Class類的物件作用**是執行時提供或獲得某個物件的型別資訊

## 反射的基本使用

### 獲取 Class 類物件

獲取反射中的Class物件有三種方法。

**第一種,使用 Class.forName 靜態方法。**
​```java
Class class1 = Class.forName("reflection.TestReflection");

第二種,使用類的.class 方法

Class class2 = TestReflection.class;

第三種,使用例項物件的 getClass() 方法。

TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();

反射創造物件,獲取方法,成員變數,構造器

本小節學習反射的基本API用法,如獲取方法,成員變數等。

反射創造物件

通過反射建立類物件主要有兩種方式:

例項程式碼:

//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);

//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);

執行結果:

反射獲取類的構造器

看一個例子吧:

Class class1 = Class.forName("reflection.Student");
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
    System.out.println(constructors[i]);
 }

反射獲取類的成員變數

看demo:

// student 一個私有屬性age,一個公有屬性email
public class Student {

    private Integer age;

    public String email;
}

public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
        Field email = class1.getField("email");
        System.out.println(email);
        Field age = class1.getField("age");
        System.out.println(age);
    }
}

執行結果:


javagetField(String name)根據引數變數名,返回一個具體的具有public屬性的成員變數,如果該變數不是public屬性,則報異常。

反射獲取類的方法

demo

public class Student {

    private void testPrivateMethod() {
        
    }
    public void testPublicMethod() {
        
    }
}

public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");

        Method[] methods = class1.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i]);
        }
    }
}

執行結果:

反射的實現原理

通過上一小節學習,我們已經知道反射的基本API用法了。接下來,跟著一個例子,學習反射方法的執行鏈路。

public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.TestReflection");
        Method method = clazz.getMethod("target", String.class);
        method.invoke(null, "666");
    }

    public static void target(String str) {
        //列印堆疊資訊
        new Exception("#" +str).printStackTrace();
        System.out.println("invoke target method");
    }
}

堆疊資訊反映出反射呼叫鏈路:

java.lang.Exception: #666
invoke target method
	at reflection.TestReflection.target(TestReflection.java:17)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at reflection.TestReflection.main(TestReflection.java:11)
​```

**invoke方法執行時序圖**

![](https://user-gold-cdn.xitu.io/2019/12/21/16f27996f9e5ed0f?w=1794&h=1030&f=png&s=154261)

**我們跟著反射鏈路去看一下原始碼,先看Method的invoke方法:**

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    //校驗許可權
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor(); //獲取MethodAccessor
    }
    //返回MethodAccessor.invoke
    return ma.invoke(obj, args);
}

由上可知道,Method 的 invoke 方法,其實是返回介面MethodAccessor的invoke方法。MethodAccessor介面有三個實現類,到底呼叫的是哪個類的 invoke 方法呢?

進入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法決定。

再進ReflectionFactory的newMethodAccessor方法,我們可以看到返回的是DelegatingMethodAccessorImpl物件,也就是說呼叫的是它的invoke方法。

再看DelegatingMethodAccessorImpl的invoke方法

DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子類NativeMethodAccessorImpl重寫,這時候返回的是本地方法invoke0,如下

因此,Method的invoke方法,是由本地方法invoke0決定的,再底層就是c++相關了,有興趣的朋友可以繼續往下研究。

反射的一些應用以及問題

反射應用

反射是Java框架的靈魂技術,很多框架都使用了反射技術,如spring,Mybatis,Hibernate等。

JDBC 的資料庫的連線

在JDBC連線資料庫中,一般包括載入驅動,獲得資料庫連線等步驟。而載入驅動,就是引入相關Jar包後,通過Class.forName() 即反射技術,載入資料庫的驅動程式。

Spring 框架的使用

Spring 通過 XML 配置模式裝載 Bean,也是反射的一個典型例子。

裝載過程:

  • 將程式內XML 配置檔案載入入記憶體中
  • Java類解析xml裡面的內容,得到相關位元組碼資訊
  • 使用反射機制,得到Class例項
  • 動態配置例項的屬性,使用

這樣做當然是有好處的:

不用每次都去new例項了,並且可以修改配置檔案,比較靈活。

反射存在的問題

效能問題

java反射的效能並不好,原因主要是編譯器沒法對反射相關的程式碼做優化。
有興趣的朋友,可以看一下這個文章java-reflection-why-is-it-so-slow

安全問題

我們知道單例模式的設計過程中,會強調將構造器設計為私有,因為這樣可以防止從外部構造物件。但是反射可以獲取類中的域、方法、構造器,修改訪問許可權。所以這樣並不一定是安全的。

看個例子吧,通過反射使用私有構造器例項化。

public class Student {
    private String name;
    private Student(String name) {
        System.out.println("我是私有構造器,我被例項化了");
        this.name = name;
    }
    public void doHomework(String subject) {
        System.out.println("我的名字是" + name);
        System.out.println("我在做"+subject+"作業");
    }
}
public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.Student");
        // 獲取私有構造方法物件
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        // true指示反射的物件在使用時應該取消Java語言訪問檢查。
        constructor.setAccessible(true);
        Student student = (Student) constructor.newInstance("jay@huaxiao");
        student.doHomework("數學");
    }
}

執行結果:


顯然,反射不管你是不是私有,一樣可以呼叫。
所以,使用反射通常需要程式的執行沒有安全限制。如果一個程式對安全性有強制要求,最好不要使用反射啦。

相關文章