【廖雪峯官方網站/Java教程】反射

注:本文參考自:https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512
什麼是反射?
反射就是Reflection,Java的反射是指程序在運行期可以拿到一個對象的所有信息。
正常情況下,如果我們要調用一個對象的方法,或者訪問一個對象的字段,通常會傳入對象實例。反射是爲了解決在運行期,對某個實例一無所知的情況下,如何調用其方法。

1.Class類

1.1.Class類和反射(reflection)基本概念

除了int等基本類型外,Java的其他類型全部都是class(包括interface)。例如:

String
Object
Runnable
Exception

而class是由JVM在執行過程中動態加載的。JVM在第一次讀取到一種class類型時,將其加載進內存。每加載一種class,JVM就爲其創建一個Class類型的實例,並關聯起來。注意:這裏的Class類型是一個名叫Class的class。它長這樣:

public final class Class {
    private Class() {}
}

以String類爲例,當JVM加載String類時,它首先讀取String.class文件到內存,然後,爲String類創建一個Class實例並關聯起來:

Class cls = new Class(String);

這個Class實例是JVM內部創建的,如果我們查看JDK源碼,可以發現Class類的構造方法是private,只有JVM能創建Class實例,我們自己的Java程序是無法創建Class實例的。
所以,JVM持有的每個Class實例都指向一個數據類型(class或interface)。
在這裏插入圖片描述
一個Class實例包含了該class的所有完整信息:
在這裏插入圖片描述
由於JVM爲每個加載的class創建了對應的Class實例,並在實例中保存了該class的所有信息,包括類名、包名、父類、實現的接口、所有方法、字段等,因此,如果獲取了某個Class實例,我們就可以通過這個Class實例獲取到該實例對應的class的所有信息。
這種通過Class實例獲取class信息的方法稱爲反射(Reflection)。

1.2.三種方法獲取一個class的Class實例

1.2.1.方法1:直接通過一個class的靜態變量class獲取:

Class cls = String.class;

1.2.2.方法2:實例變量的getClass()方法

如果我們有一個實例變量,可以通過該實例變量提供的getClass()方法獲取:

String s = "Hello";
Class cls = s.getClass();

1.2.3.方法3:靜態方法Class.forName()獲取

如果知道一個class的完整類名,可以通過靜態方法Class.forName()獲取:

Class cls = Class.forName("java.lang.String");

1.3.Class實例比較

1.3.1.利用"=="比較兩個Class實例

因爲Class實例在JVM中是唯一的,所以,上述方法獲取的Class實例是同一個實例。可以用==比較兩個Class實例:

Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true

1.3.2.與instanceof的區別

兩者區別如下:

Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因爲n是Integer類型
boolean b2 = n instanceof Number; // true,因爲n是Number類型的子類

boolean b3 = n.getClass() == Integer.class; // true,因爲n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因爲Integer.class!=Number.class

用instanceof不但匹配指定類型,還匹配指定類型的子類。而用"==“判斷class實例可以精確地判斷數據類型,但不能作子類型比較。
通常情況下,我們應該用instanceof判斷數據類型,因爲面向抽象編程的時候,我們不關心具體的子類型。只有在需要精確判斷一個類型是不是某個class的時候,我們才使用”=="判斷class實例。

1.4.數組也是一種Class

注意到數組(例如String[])也是一種Class,而且不同於String.class,它的類名是[Ljava.lang.String。此外,JVM爲每一種基本類型如int也創建了Class,通過int.class訪問。
如果獲取到了一個Class實例,我們就可以通過該Class實例來創建對應類型的實例:

// 獲取String的Class實例:
Class cls = String.class;
// 創建一個String實例:
String s = (String) cls.newInstance(); // 相當於 new String()

上述代碼相當於new String()。通過Class.newInstance()可以創建類實例,它的侷限是:只能調用【public && 無參數】構造方法。帶參數的構造方法,或者非public的構造方法都無法通過Class.newInstance()被調用。

1.5.動態加載

JVM在執行Java程序的時候,並不是一次性把所有用到的class全部加載到內存,而是第一次需要用到class時才加載。

2.訪問字段

2.1.Class類中訪問字段

對任意的一個Object實例,只要我們獲取了它的Class,就可以獲取它的一切信息。
我們先看看如何通過Class實例獲取字段信息。Class類提供了以下幾個方法來獲取字段:

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

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

  • getName():返回字段名稱,類型是String,例如,“name”;
  • getType():返回字段類型,也是一個Class實例,例如,String.class;
  • getModifiers():返回字段的修飾符,它是一個int,不同的bit表示不同的含義。
    以String類的value字段爲例,它的定義是:
public final class String {
    private final byte[] value;
}

我們用反射獲取該字段的信息,代碼如下:

Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]類型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false

2.2.獲取字段值

利用反射拿到字段的一個Field實例只是第一步,我們還可以拿到一個實例對應的該字段的值。
例如,對於一個Person實例,我們可以先拿到name字段對應的Field,再獲取這個實例的name字段的值:

// reflection
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Object p = new Person("Xiao Ming");
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
        Object value = f.get(p);
        System.out.println(value); // "Xiao Ming"
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
}

上述代碼先獲取Class實例,再獲取Field實例,然後,用Field.get(Object)獲取指定實例的指定字段的值。
運行代碼,如果不出意外,會得到一個IllegalAccessException,這是因爲name被定義爲一個private字段,正常情況下,Main類無法訪問Person類的private字段。要修復錯誤,可以將private改爲public,或者,在調用Object value = f.get§;前,先寫一句:

f.setAccessible(true);

調用Field.setAccessible(true)的意思是,別管這個字段是不是public,一律允許訪問。
可以試着加上上述語句,再運行代碼,就可以打印出private字段的值。

2.3.設置字段值

通過Field實例既然可以獲取到指定實例的字段值,自然也可以設置字段的值。
設置字段值是通過Field.set(Object, Object)實現的,其中第一個Object參數是指定的實例,第二個Object參數是待修改的值。示例代碼如下:

// reflection
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Xiao Ming");
        System.out.println(p.getName()); // "Xiao Ming"
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
        f.setAccessible(true);
        f.set(p, "Xiao Hong");
        System.out.println(p.getName()); // "Xiao Hong"
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
}

運行上述代碼,打印的name字段從Xiao Ming變成了Xiao Hong,說明通過反射可以直接修改字段的值。
同樣的,修改非public字段,需要首先調用setAccessible(true)。

3.調用方法

3.1.Class類中獲取Method

我們已經能通過Class實例獲取所有Field對象,同樣的,可以通過Class實例獲取所有Method信息。Class類提供了以下幾個方法來獲取Method:

  • Method getMethod(name, Class…):獲取某個public的Method(包括父類) Method
  • getDeclaredMethod(name, Class…):獲取當前類的某個Method(不包括父類) Method[]
  • getMethods():獲取所有public的Method(包括父類) Method[]
  • getDeclaredMethods():獲取當前類的所有Method(不包括父類)
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 獲取public方法getScore,參數爲String:
        System.out.println(stdClass.getMethod("getScore", String.class));
        // 獲取繼承的public方法getName,無參數:
        System.out.println(stdClass.getMethod("getName"));
        // 獲取private方法getGrade,參數爲int:
        System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
    }
}

class Student extends Person {
    public int getScore(String type) {
        return 99;
    }
    private int getGrade(int year) {
        return 1;
    }
}

class Person {
    public String getName() {
        return "Person";
    }
}

上述代碼首先獲取Student的Class實例,然後,分別獲取public方法、繼承的public方法以及private方法,打印出的Method類似:

public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)

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

  • getName():返回方法名稱,例如:“getScore”;
  • getReturnType():返回方法返回值類型,也是一個Class實例,例如:String.class;
  • getParameterTypes():返回方法的參數類型,是一個Class數組,例如:{String.class, int.class};
  • getModifiers():返回方法的修飾符,它是一個int,不同的bit表示不同的含義。

3.2.調用方法(通過反射執行Method)

當我們獲取到一個Method對象時,就可以對它進行調用。我們以下面的代碼爲例:

String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射來調用substring方法,需要以下代碼:

public class Main {
    public static void main(String[] args) throws Exception {
        // String對象:
        String s = "Hello world";
        // 獲取String substring(int)方法,參數爲int:
        Method m = String.class.getMethod("substring", int.class);
        // 在s對象上調用該方法並獲取結果:
        String r = (String) m.invoke(s, 6);
        // 打印調用結果:
        System.out.println(r);
    }
}

注意到substring()有兩個重載方法,我們獲取的是String substring(int)這個方法。思考一下如何獲取String substring(int, int)方法。

Method m = String.class.getMethod("substring", int.class, int.class);
// 在s對象上調用該方法並獲取結果:
String r = (String) m.invoke(s, 4, 6);

對Method實例調用invoke就相當於調用該方法,invoke的第一個參數是對象實例,即在哪個實例上調用該方法,後面的可變參數要與方法參數一致,否則將報錯。

3.3.調用靜態方法

如果獲取到的Method表示一個靜態方法,調用靜態方法時,由於無需指定實例對象,所以invoke方法傳入的第一個參數永遠爲null。我們以Integer.parseInt(String)爲例:

public class Main {
    public static void main(String[] args) throws Exception {
        // 獲取Integer.parseInt(String)方法,參數爲String:
        Method m = Integer.class.getMethod("parseInt", String.class);
        // 調用該靜態方法並獲取結果:
        Integer n = (Integer) m.invoke(null, "12345");
        // 打印調用結果:
        System.out.println(n);
    }
}

3.4.調用非public方法

和Field類似,對於非public方法,我們雖然可以通過Class.getDeclaredMethod()獲取該方法實例,但直接對其調用將得到一個IllegalAccessException。爲了調用非public方法,我們通過Method.setAccessible(true)允許其調用:

// reflection
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name);
    }
}

class Person {
    String name;
    private void setName(String name) {
        this.name = name;
    }
}

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

3.5.多態

我們來考察這樣一種情況:一個Person類定義了hello()方法,並且它的子類Student也覆寫了hello()方法,那麼,從Person.class獲取的Method,作用於Student實例時,調用的方法到底是哪個?

// reflection
import java.lang.reflect.Method;

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

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

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

運行上述代碼,發現打印出的是Student:hello,因此,使用反射調用方法時,仍然遵循多態原則:即總是調用實際類型的覆寫方法(如果存在)。上述的反射代碼:

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

實際上相當於:

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

4.調用構造方法

此部分可結合《Java從入門到精通》第16章的內容一起看。
我們通常使用new操作符創建新的實例:

Person p = new Person();

如果通過反射來創建新的實例,可以調用Class提供的newInstance()方法:

Person p = Person.class.newInstance();

調用Class.newInstance()的侷限是,它只能調用該類的public無參數構造方法。如果構造方法帶有參數,或者不是public,就無法直接通過Class.newInstance()來調用。
爲了調用任意的構造方法,Java的反射API提供了Constructor對象,它包含一個構造方法的所有信息,可以創建一個實例。Constructor對象和Method非常類似,不同之處僅在於它是一個構造方法,並且,調用結果總是返回實例:

import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception {
        // 獲取構造方法Integer(int):
        Constructor cons1 = Integer.class.getConstructor(int.class);
        // 調用構造方法:
        Integer n1 = (Integer) cons1.newInstance(123);
        System.out.println(n1);

        // 獲取構造方法Integer(String)
        Constructor cons2 = Integer.class.getConstructor(String.class);
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}

通過Class實例獲取Constructor的方法如下:

  • getConstructor(Class…):獲取某個public的Constructor;
  • getDeclaredConstructor(Class…):獲取某個Constructor;
  • getConstructors():獲取所有public的Constructor;
  • getDeclaredConstructors():獲取所有Constructor。

注意Constructor總是當前類定義的構造方法,和父類無關,因此不存在多態的問題。
調用非public的Constructor時,必須首先通過setAccessible(true)設置允許訪問。setAccessible(true)可能會失敗。

5.獲取繼承關係

在1.2小節中總結了獲取一個類Class實例的3種方法。

5.1.獲取父類的Class

有了Class實例,我們還可以獲取它的父類的Class:

public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}

運行上述代碼,可以看到,Integer的父類類型是Number,Number的父類是Object,Object的父類是null。除Object外,其他任何非interface的Class都必定存在一個父類類型。

5.2.獲取interface

由於一個類可能實現一個或多個接口,通過Class我們就可以查詢到實現的接口類型。例如,查詢Integer實現的接口:

// reflection
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class;
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}

要特別注意:getInterfaces()只返回當前類直接實現的接口類型,並不包括其父類實現的接口類型。
此外,對所有interface的Class調用getSuperclass()返回的是null,獲取接口的父接口要用getInterfaces():

System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因爲DataInputStream繼承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,對接口調用getSuperclass()總是返回null,獲取接口的父接口要用getInterfaces()

如果一個類沒有實現任何interface,那麼getInterfaces()返回空數組。

5.3.繼承關係

當我們判斷一個實例是否是某個類型時,正常情況下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是兩個Class實例,要判斷一個向上轉型是否成立,可以調用isAssignableFrom():

// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因爲Number不能賦值給Integer

6.動態代理

6.1.靜態代碼寫法

有沒有可能不編寫實現類,直接在運行期創建某個interface的實例呢?
這是可能的,因爲Java標準庫提供了一種動態代理(Dynamic Proxy)的機制:可以在運行期動態創建某個interface的實例。
什麼叫運行期動態創建?聽起來好像很複雜。所謂動態代理,是和靜態相對應的。我們來看靜態代碼怎麼寫:
定義接口:

public interface Hello {
    void morning(String name);
}

編寫實現類:

public class HelloWorld implements Hello {
    public void morning(String name) {
        System.out.println("Good morning, " + name);
    }
}

創建實例,轉型爲接口並調用:

Hello hello = new HelloWorld();
hello.morning("Bob");

6.2.動態代理【未細看】

還有一種方式是動態代碼,我們仍然先定義了接口Hello,但是我們並不去編寫實現類,而是直接通過JDK提供的一個Proxy.newProxyInstance()創建了一個Hello接口對象。這種沒有實現類但是在運行期動態創建了一個接口對象的方式,我們稱爲動態代碼。JDK提供的動態創建接口對象的方式,就叫動態代理。
一個最簡單的動態代理實現如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 傳入ClassLoader
            new Class[] { Hello.class }, // 傳入要實現的接口
            handler); // 傳入處理調用方法的InvocationHandler
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

在這裏插入圖片描述
動態代理實際上是JDK在運行期動態創建class字節碼並加載的過程,它並沒有什麼黑魔法,把上面的動態代理改寫爲靜態實現類大概長這樣:

public class HelloDynamicProxy implements Hello {
    InvocationHandler handler;
    public HelloDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    public void morning(String name) {
        handler.invoke(
           this,
           Hello.class.getMethod("morning"),
           new Object[] { name });
    }
}

其實就是JDK幫我們自動編寫了一個上述類(不需要源碼,可以直接生成字節碼),並不存在可以直接實例化接口的黑魔法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章