學習反射看這一篇就夠了


關注公眾號:小李不禿

1. 反射

Java 分編譯期和運行期

編譯方式說明:

  1. 靜態編譯:在編譯時確定類型 & 綁定對象。如常見的使用new關鍵字創建對象
  2. 動態編譯:運行時確定類型 & 綁定對象。動態編譯體現了Java的靈活性、多態特性 & 降低類之間的耦合性

我們帶着以下幾個問題去學習今天的知識

  • 反射是什么?
  • 反射的作用是什么?
  • 反射的優點是什么?缺點是什么?

1.1 什么是反射

反射(Reflection)是 Java 的特性之一,它可以讓運行中的 Java 程序獲取自身的信息,並且可以操作類或者對象的內部屬性。

Oracle 官方對反射的解釋是:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.

The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

通過反射,我們可以在程序運行時獲得程序集中每一個類型的成員和成員信息。我們平時所用的 new 去創建的對象的類型,是在編譯期就確定下來了。而 Java 反射可以動態地創建對象並調用其屬性,這樣的對象的類型在編譯期是未知的。所以我們可以通過反射機制創建對象,即使這個對象的類型在編譯期是未知的。

反射的核心是 JVM 在運行時才會動態加載類、調用方法和訪問屬性,它不需要在編譯期知道運行的對象是誰。

Java 反射主要提供以下的功能:

  • 在運行時判斷任意一個對象所屬的類
  • 在運行時構造任意一個類的對象
  • 在運行時判斷任意一個類所具有的成員變量和方法
  • 在運行時調用任意一個對象的方法

我們可以在運行時取到「任意」你想要的類、對象、變量、方法等。

注:反射是在運行時操作的,而不是編譯時

1.2 反射的主要用途

  • 實現工廠模式和代理模式等設計模式。
  • JDBC 的數據庫連接。
  • Spring、Struts 等框架,使用反射在運行時動態加載需要加載的對象。
  • IDE 開發工具的提示,比如當我們輸入一個對象或類並想調用它的屬性或方法時,一按點號,編譯器就會自動列出它的屬性或方法。

1.3 反射的優缺點

優點:

  • 可以在運行期對類型進行判斷,動態類加載等操作。
  • 提高代碼的靈活度。例如:JDBC 可以動態連接數據庫。

缺點:

  • 性能問題

    因為反射包括了一些動態類型,所以 JVM 無法對這些代碼進行優化。因此,反射操作的效率要比那些直接調用慢的多。所以盡量避免在經常被執行的代碼或者對性能要求很高的程序中使用反射。

    (反射大概比直接調用慢 50 ~ 100 倍,但是需要在執行 100 萬遍的時候才會有所感覺)

  • 安全限制

    使用反射技術要求程序必須在一個沒有安全限制的環境中運行。

  • 內部暴漏

    反射允許代碼執行一些在正常情況下不被允許的操作(訪問私有的屬性或方法),所以使用反射可能會導致意料之外的副作用 —— 代碼有功能上的錯誤,降低可移植性。反射破壞了代碼的抽象性,因此當平台發生改變的時候,代碼的行為就可能也隨着變化。

提問:Java 反射可以訪問和修改私有成員變量,那封裝成 private 還有意義么?

既然小偷可以訪問和搬走私有成員家具,那封裝成防盜門還有意義么?這是一樣的道理,並且 Java 從應用層給我們提供了安全管理機制——安全管理器,每個 Java 應用都可以擁有自己的安全管理器,它會在運行階段檢查需要保護的資源的訪問權限及其它規定的操作權限,保護系統免受惡意操作攻擊,以達到系統的安全策略。

所以其實反射在使用時,內部有安全控制,如果安全設置禁止了這些,那么反射機制就無法訪問私有成員。

1.4 具體使用

1.4.1 Class

Class 存放着對應類型的運行時信息。在 Java 程序運行時,Java 虛擬機為所有類型維護一個 java.lang.Class 對象。該 Class 對象存放着所有關於該對象的運行時信息。

1.4.1.1 獲取 Class

那我們如何獲取想要的 Class,look this Code

    public static void main(String[] args) {
        // 方式1:Object.getClass()
        // Object 類的 getClass() 方法返回一個 Class 類實例
        String name = "不是禿頭的小李程序員";
        Class<?> classType = name.getClass();
        System.out.println("Object.getClass() classType: " + classType);

        // 方式2:T.Class
        // T 是任意 Java 類型
        Class<?> classType2 = String.class;
        System.out.println("T.Class classType: " + classType2);

        // 方式3:Class.forName
        try {
            Class<?> classType3 = Class.forName("java.lang.String");
            System.out.println("Class.forName classType: " + classType2);
            // 根據 className 沒有找到類,會拋出 ClassNotFoundException 異常
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

獲取 Class 的三種方法

  • getClass()
  • T.class
  • Class.forName

這三種用法需要根據具體的場景靈活去使用,比如 JDBC 獲取連接的數據庫類型,就通過 Class.forName(“類路徑”)。

1.4.1.2 獲取父類 Class

通過 getSuperclass() 方法獲取父類的 Class,示例如下:

Class<?> superclass = Integer.class.getSuperclass();
System.out.println(superclass);
System.out.println(superclass.getSuperclass());
System.out.println(superclass.getSuperclass().getSuperclass());
System.out.println(superclass.getSuperclass().getSuperclass().getSuperclass());

運行結果

class java.lang.Number
class java.lang.Object
null
Exception in thread "main" java.lang.NullPointerException

可以看到 Integer 的父類是 Number,Number 的父類是 Object,Object 就沒有父類了,所以在 null 之后會拋出空指針的異常。

1.4.1.3 小結

獲取到想要的 Class 了,就可以獲取到它的一切信息。

在獲取想要的信息前,不妨先了解個知識點。

方法中帶與不帶「Declared」的區別

  1. 不帶「Declared」的方法支持取出包括繼承、公有(public)的 Field、Method 和 Constructor。
  2. 帶「Declared」的方法支持取出包括當前類所有的構造函數(包括公有和私有的,不包括繼承)的 Field、Method 和 Constructor。

1.4.2 Field

1.4.2.1 獲取 Field

如何通過 Class 實例獲取字段信息。Class 類提供了以下幾個方法來獲取字段:

  • Field getField(String name):根據字段名獲取某個 public 的 field
  • Field[] getFields():獲取所有 public 的 field
  • Field getDeclaredField(String name):根據字段名獲取當前類的某個 field
  • Field[] getDeclaredFields():獲取當前類的所有 field

快快快,show me code

public class FiledTest1 {
    public static void main(String[] args) throws NoSuchFieldException {
        Class stdClass = Student.class;
        // 獲取 public 字段 "score"
        System.out.println(stdClass.getField("score"));
        // 獲取繼續的 public 字段 "name"
        System.out.println(stdClass.getField("name"));
        // 獲取 private 字段 "grade"
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student extends Person{
    public int score;
    private int grade;
}

class Person{
    public String name;
}

運行結果:

public int com.javastudy.reflection.Fields.Student.score
public java.lang.String com.javastudy.reflection.Fields.Person.name
private int com.javastudy.reflection.Fields.Student.grade

1.4.2.2 獲取 Field 的信息

一個 Filed 對象包含了一個字段的所有信息:

  • getName():返回字段名稱,例如:name
  • getType():返回字段類型,也是一個 Class 實例,例如:String.class
  • getModifiers():返回字段的修飾符,它是一個 int,不同的 bit 表示不同的含義。

The java.lang.reflect.Method.getModifiers() method returns the Java language modifiers for the method represented by this Method object, as an integer. The Modifier class should be used to decode the modifiers.

getmodifiers()方法以整數的形式返回該方法對象所表示的方法的 Java 語言修飾符。應該使用修飾詞類來解碼修飾詞。

public class FieldTest2 {

    private final String name = "不是禿頭的小李程序員";

    public static void main(String[] args) throws NoSuchFieldException {
        Class c = FieldTest2.class;
        Field field = c.getDeclaredField("name");
        int mod = field.getModifiers();
        System.out.println("name: " + field.getName());
        System.out.println("type: " + field.getType());
        System.out.println("final: " + Modifier.isFinal(mod));
        System.out.println("public: " + Modifier.isPublic(mod));
        System.out.println("protected: " + Modifier.isProtected(mod));
        System.out.println("private: " + Modifier.isPrivate(mod));
        System.out.println("static: " + Modifier.isStatic(mod));
    }
}

運行結果:

name: name
type: class java.lang.String
finaltrue
publicfalse
protectedfalse
privatetrue
staticfalse

1.4.2.3 獲取字段值

我們拿到了 Field,該通過 Field 獲取該字段對應的值。我們還是用上面的例子來獲取 name 值。

public class FieldTest3 {

    private final String name = "不是禿頭的小李程序員";

    public static void main(String[] args) throws Exception {
        Object object = new FieldTest3();
        Class c = FieldTest3.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(object);
        System.out.println(value);
    }
}

運行結果:

不是禿頭的小李程序員

我們通過 get() 來獲取 Field 的值,那么咱們在看一個下面的例子:

public class FieldTest4 {

    public static void main(String[] args) throws Exception 
        Object animal = new Animal("不是禿頭的小李程序員 Animal111");
        Class c = Animal.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(animal);
        System.out.println(value);
//        Animal animal = new Animal();
//        animal.testFiled();
    }
}

class Animal {
    private String name;

    public Animal() {
    }

    public Animal(String name){
        this.name = name;
    }

    public void testFiled() throws Exception {
        Object animal = new Animal("不是禿頭的小李程序員 Animal222");
        Class c = Animal.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(animal);
        System.out.println(value);
    }
}

運行結果:

Exception in thread "main" java.lang.IllegalAccessException: Class com.javastudy.reflection.Fields.FieldTest4 can not access a member of class com.javastudy.reflection.Fields.Animal with modifiers "private"

WTF?竟然出現異常了,小李你是在玩我么,第一次就可以,第二次又提示沒有權限,到底能不能獲取我想要的值。當然能,不能我就禿給你看。

我們只需要在 filed.get() 的上一步加上下面的代碼就可以了。管你是 public 還是 private.

field.setAccessible(true);

那我們想一想為什么第一次不加上面的代碼也能訪問呢?

因為是在自己的類里訪問的,就拿你自己想一想,你有一個鼻子和兩個耳朵,它們就是你私有的(private),你可以隨便摸他們和摳他們(嘔),但是別人想碰的時候,必須要經過你的同意才行(setAccessible(true))。這么理解下剛才的代碼,你就明白了。如果還不明白可以打開上面的兩段注釋:

Animal animal = new Animal();
animal.testFiled();

運行結果:

不是禿頭的小李程序員222

1.4.2.4 設置字段值

通過 Field 實例既能獲取指定實例的字段值,也可以設置字段的值。

設置字段值通過 Fieldset 方法實現。

// 第一個參數是指定的實例
// 第二個參數是待修改的值
void set(Object obj, Object value)

示例代碼如下:

public class FieldTest5 {
    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher("不是禿頭的小李程序員");
        Class c = teacher.getClass();
        Field field = c.getDeclaredField("name");
        field.setAccessible(true);
        field.set(teacher,"小李不禿頭");
        System.out.println(field.get(teacher));
    }
}
class Teacher{
    private String name;

    public Teacher(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

打印結果:

小李不禿頭

為了不禿頭,我容易么(此處嚶嚶嚶)。嚶完就該提問題了

Field 在 get 和 set 的 obj 參數有什么作用?

回答:我們可以通過查看 api 的注釋,了解到這兩個方法在獲取靜態實例的時候,obj 可以傳 null,如果要獲取對象的實例 obj 參數就不能為空,否則會返回 NullException。

1.4.2.5 小結

Java 的反射 API 提供的 Field 類封裝了字段的所有信息:

  • 通過 Class 實例獲取 Field 實例的方法:getField(String name),getFields(),getDeclaredField(),getDeclaredFields()
  • 通過 Field 實例獲取字段信息的方法:getName(),getType(),getModifiers()
  • 通過 Field 實例可以讀取或者設置某個對象的字段,如果存在訪問限制,首先要調用 setAccessible(true),再訪問非 public 字段。

1.4.3 Method

1.4.3.1 獲取 Method

通過 Class 實例獲取所有 Method 的信息,Class 類提供了以下幾個方法來獲取方法:

  • Method getMethod(String name, Class … parameterTypes):根據方法名和參數類型獲取某個 public 的 Method

  • Method[] getMethods():獲取所有 public 的 Method

  • Method getDeclaredMethod(String name, Class … parameterTypes):根據方法名和參數獲取當前類的某個 Method

  • Method[] getDeclaredMethods():獲取當前類的所有 Method

看一下示例代碼:

public class MethodTest1 {
    public static void main(String[] args) throws Exception{
        Class c = Student.class;
        // 獲取public方法getScore,參數為String;
        System.out.println(c.getMethod("getScore",String.class));
        // 獲取繼承的public方法getName,無參數;
        System.out.println(c.getMethod("getName"));
        // 獲取private方法getGrade,參數為int;
        System.out.println(c.getDeclaredMethod("getGrade",int.class));
    }
}

運行結果:

public int com.javastudy.reflection.Methods.Student.getScore(java.lang.String)
public java.lang.String com.javastudy.reflection.Methods.Person.getName()
private int com.javastudy.reflection.Methods.Student.getGrade(int)

1.4.3.2 獲取 Method 的信息

一個 Method 對象包含了一個方法的所有信息:

  • getName():返回方法名稱,例如:「getScore」
  • getReturnType():返回方法的返回值類型,是一個 Class 實例,例如:「String.class」
  • getParameterTypes():返回方法的參數類型,是一個 Class 數組,例如:{String.class, int.class}
  • getModifiers():返回方法的修飾符,和 Field 的 getModifiers() 大同小異

示例如下:

public class MethodTest2 {
    public static void main(String[] args) throws Exception{
        Class c = Student.class;
        Method method= c.getDeclaredMethod("getGrade",int.class);

        System.out.println("name : " + method.getName());
        System.out.println("returnType : " + method.getReturnType());
        Class<?>[] parameterTypes = method.getParameterTypes();
        System.out.println("paramaterTypes的長度 : " + parameterTypes.length);
        for (Class parameterType : parameterTypes){
            System.out.println("paramaterTypes : " + parameterType);
        }
    }
}

運行結果:

name : getGrade
returnType : int
paramaterTypes的長度 : 1
paramaterTypes : int

1.4.3.3 調用方法

調用普通方法

先看示例:

public class MethodTest3 {
    public static void main(String[] args) throws Exception {
        String s = "不是禿頭的小李程序員";
        Method method = String.class.getMethod("substring"int.class);
        Method method2 = String.class.getMethod("substring"int.class, int.class);
        String result = (String) method.invoke(s,7);
        String result2 = (String) method2.invoke(s,1,9);
        System.out.println(result);
        System.out.println(result2);
    }
}

運行結果:
程序員
是禿頭的小李程序

分析下程序員小李是如何禿頭的:

  1. 通過 Class 實例的 getMethod 方法獲取Method,getMethod 的 name 和參數不同,獲取的 Method 也是不同的。
  2. 使用 Method 的 invoke 方法就相當於調用該方法。invoke 的第一個參數是對象實例,后面的可變參數與方法參數一致,否則將報錯。
調用靜態方法

調用靜態方法,無需指定實例對象,invoke 方法傳入的第一個參數永遠是 null 或者空值 ”“,我們看下面的例子:

public class MethodTest4 {
    public static void main(String[] args) throws Exception{
        // 獲取Integer.parseInt(Stirng)方法,參數是String
        Method method = Integer.class.getMethod("parseInt", String.class);
        // 調用靜態方法獲取結果
        // Integer result = (Integer)method.invoke("", "12345");
        Integer result = (Integer)method.invoke(null"12345");
        System.out.println(result);
    }
}

運行結果:12345
調用非public方法

對於非public方法,我們可以通過 Class.getDeclaredMethod() 獲取,但是調用的時候會拋出一個IllegalAccessException。為了調用非public方法,通過Method.setAccessible(true)允許其調用:

public class MethodTest5 {
    public static void main(String[] args) throws Exception{
        Person p = new Person();
        Method method = p.getClass().getDeclaredMethod("setName", String.class);
        method.setAccessible(true);
        method.invoke(p,"不是禿頭的小李程序員");
        System.out.println(p.name);
    }
}

此外,setAccessible(true)可能會失敗。如果JVM運行期存在SecurityManager,那么它會根據規則進行檢查,有可能阻止setAccessible(true)。例如,某個SecurityManager可能不允許對javajavax開頭的package的類調用setAccessible(true),這樣可以保證JVM核心庫的安全。

1.4.3.4 多態

如果一個 Person 定義了 hello() 方法,並且它的子類Student也重寫了該方法,那么我們從 Person.class 獲取的 Method 作用於 Student 實例時,調用的方法是哪個?

public class MethodTest6 {
    public static void main(String[] args) throws Exception{
        // 獲取Person的hello方法
        Method method = Person.class.getMethod("hello");
        // 對Student實例調用hello方法
        method.invoke(new Student());
    }
}

public class Person {
    public void hello(){
        System.out.println("Person:hello");
    }
}

public class Student extends Person {
    public void hello(){
        System.out.println("Student:hello");
    }
}

1.4.3 .5運行結果

Student:hello

發現打印出的是Student:hello,所以使用反射調用方法時,仍遵循多態原則:即總是調用實際類型的覆蓋方法。

上述的反射代碼:

Method m = Person.class.getMethod("hello");
m.invoke(new Student());

相當於:

Person p = new Student();
p.hello();

1.4.3.5 小結

Java 的反射 API 提供的 Method 對象封裝了方法的所有信息:

  • 通過 Class 實例獲取 Method 實例的方法:getMethod(),getMethods(),getDeclaredMethod(),getDeclaredMethods()
  • 通過 Method 實例獲取字段信息的方法:getName(),getReturnType(),getParameterTypes(),getModifiers()
  • 通過 Method 實例可以調用某個對象的方法:Object invoke(Object instance, Object… parameters)
  • 通過設置 setAccessible(true) 來訪問非 public 方法
  • 通過反射調用方法時,仍能遵循多態原則

1.4.4 Constructor

1.4.4.1 獲取 Constructor

通過 Class 實例獲取所有 Constructor 的信息,Class 類提供了以下幾個方法來獲取方法:

  • Constructor getConstructor(Class … parameterTypes):根據參數獲取 public 的 Contructor

  • Constructor [] getConstructors():獲取所有 public 的 Contructor

  • Constructor getDeclaredConstructor(Class … parameterTypes):根據參數獲取當前類的 Contructor

  • Constructor [] getDeclaredConstructors():獲取所有當前類的 Contructor

Constructor 總是當前類定義的構造方法,和父類無關,因此不存在多態的問題。

示例如下:

public class ContructorTest1 {
    public static void main(String[] args) throws Exception{
        Class c = Person.class;
        Person p = (Person) c.newInstance();

        Constructor cons1 = c.getConstructor(int.class);
        Person p1 = (Person)cons1.newInstance(30);

        Constructor cons2 = c.getDeclaredConstructor(String.class);
        cons2.setAccessible(true);
        Person p2 = (Person)cons2.newInstance("不是禿頭的小李程序員");

        Constructor cons3 = c.getConstructor(String.class, int.class);
        Person p3 = (Person)cons3.newInstance("不是禿頭的小李程序員-35",35);
    }
}
Person.class

public class Person {
    private String name;
    private int age;

    public Person() {
        System.out.println("Person");
    }

    public Person(int age) {
        this.age = age;
        System.out.println("Person age:" + age);
    }

    private Person(String name) {
        this.name = name;
        System.out.println("Person name:" + name);
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person toString:" + toString());
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

運行結果:

Person
Person age:30
Person name:不是禿頭的小李程序員
Person toString:Person{name='不是禿頭的小李程序員-35', age=35}

1.4.4.2 小結

通過上面的結果,得出以下結論:

  1. 通過 Class 實例的 newInstance() 獲取到的是無參構造函數
  2. 獲取有參構造函數需要通過 Class 實例獲取 Constructor 實例,獲取方法:getConstructor(),getConstructors(),getDeclaredConstructor(Class … parameterTypes),getDeclaredConstructors()
  3. 通過 Constructor 的 newInstance(Object… parameters) 創建實例對象
  4. 調用非 public 的 Contructor,需要通過設置 setAccessible(true) 設置允許訪問,但可能會失敗。

1.4.5 Interface

我們可以通過 Class 的 getInterfaces() 獲取當前類實現的所有接口,示例如下:

public class ReflectionInterfaceTest {
    public static void main(String[] args) {
        Class s = Integer.class;
        Class[] interfaces = s.getInterfaces();
        for (Class c:interfaces){
            System.out.println(c);
        }
    }
}

運行結果:

interface java.lang.Comparable

2. 總結

通過本文你大概了解了反射,讓我們再復習一下:

  • 反射是 Java 的特性之一,可以通過反射動態的獲取對象。
  • 反射的用途:代理模式等設計模式;通過 JDBC 連接數據庫;Spring 框架動態加載對象
  • 反射的優點:動態加載、提高代碼的靈活度
  • 反射的缺點:性能問題、安全限制、內部暴漏
  • 反射的使用:通過獲取到 Class 實例,就能獲取到我們想要的所有信息,包括獲取成員變量、方法和構造函數,分別對應的是 Field,Method 和 Constructor。可以通過這些類的內部方法獲取信息,例如:通過getName() 獲取名稱。
  • 如果我們修改或者訪問帶有 private 的變量或方法,需要先設置 method.setAccessible(true),才可以進行后續的操作。

下一節我帶你們看看反射的原理,讓我們更進一步的了解它。

3. 參考

https://blog.csdn.net/carson_ho/article/details/80921333

https://zhidao.baidu.com/question/1639472919001036380.html

https://www.liaoxuefeng.com/wiki/1252599548343744/1264803033837024


免責聲明!

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



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