Java中的反射

什么是反射

Java 反射是可以讓我們在運行時獲取類的函數、屬性、父類、接口等 Class 內部信息的機制。

能做什么

反射可以訪問或者修改程序的運行時行為

通過反射還可以讓我們在運行期實例化對象,調用方法,通過調用 get/set 方法獲取變量的值,即使方法或屬性是私有的的也可以通過反射的形式調用,這種“看透 class”的能力被稱為內省,這種能力在框架開發中尤為重要。 有些情況下,我們要使用的類在運行時才會確定,這個時候我們不能在編譯期就使用它,因此只能通過反射的形式來使用在運行時才存在的類(該類符合某種特定的規范,例如 JDBC),這是反射用得比較多的場景。

還有一個比較常見的場景就是編譯時我們對于類的內部信息不可知,必須得到運行時才能獲取類的具體信息。比如 ORM 框架,在運行時才能夠獲取類中的各個屬性,然后通過反射的形式獲取其屬性名和值,存入數據庫。這也是反射比較經典應用場景之一。

核心API

  • java.lang.Class.java:類本身
  • java.lang.reflect.Constructor.java:類的構造器
  • java.lang.reflect.Method.java:類的方法
  • java.lang.reflect.Field.java:類的屬性

例子

為了說明方便,先寫了幾個類

Person類

public class Person {

    String name;
    String sex;
    public int age;
    
    public Person(String name, String sex, int age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    private String getDescription() {
        return "黃種人";
    }
}

ICompany接口

public interface ICompany {

    String getCompany();
    boolean isTopCompany();
}

繼承 Person 實現 ICompany 接口的 ProgrameMonkey 類

public class ProgrameMonkey extends Person implements ICompany {
    
    private String language = "C#";
    private String company = "BBK";

    public ProgrameMonkey(String name, String sex, int age) {
        super(name, sex, age);
    }
    
    public ProgrameMonkey(String name, String sex, int age, String language, String company) {
        super(name, sex, age);
        this.language = language;
        this.company = company;
    }

    public String getLanguage() {
        return language;
    }

    public void setLanguage(String language) {
        this.language = language;
    }

    public void setCompany(String company) {
        this.company = company;
    }
    
    private int getSalaryPerMonth() {
        return 123456;
    }

    @Override
    public String getCompany() {
        return company;
    }

    @Override
    public boolean isTopCompany() {
        return true;
    }
}

Class

三種獲取類信息的方式

  • 通過 類名.class 的方式
Class<?> classObj = ProgrameMonkey.class;
  • 通過調用 類的實例.getClass() 方法的方式
ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
Class<?> classObj = programeMonkey.getClass();
  • 通過 Class.forName(完整類名) 的方式
Class<?> aClassObj = Class.forName("com.okada.reflect.ProgrameMonkey");

利用反射實例化一個對象

對于構造方式是私有的類 要怎么實例化

獲取父類

Class<?> superclass = ProgrameMonkey.class.getSuperclass();
while (superclass != null) {
    System.out.println(superclass.getName());
    superclass = superclass.getSuperclass();
}

打印結果

com.okada.reflect.Person
java.lang.Object

獲取實現的接口

Class<?>[] interfaces = ProgrameMonkey.class.getInterfaces();
for (Class<?> itf : interfaces) {
    System.out.println(itf.getName());
}

打印結果

com.okada.reflect.ICompany

Constructor

獲取 public 修飾的構造器

根據參數列表,調用相應的構造器,實例化一個對象。注意一下,getConstructor() 獲取的是用 public 修飾的構造器

Constructor<ProgrameMonkey> constructor = ProgrameMonkey.class
    .getConstructor(String.class, String.class, int.class);
ProgrameMonkey programeMonkey = constructor.newInstance("小明", "男", 18);

獲取私有的構造器

如果構造器是私有的怎么辦?

Constructor<ProgrameMonkey> constructor = ProgrameMonkey.class
        .getDeclaredConstructor(String.class, String.class, int.class);
constructor.setAccessible(true);
ProgrameMonkey programeMonkey = constructor.newInstance("小明", "男", 18);
System.out.println(programeMonkey.getName());

要調用 getDeclaredConstructor(方法簽名定義) 來獲取 Constructor,同時還要調用 constructor.setAccessible(true)

Method

獲取類的方法

獲取當前類以及父類和接口的所有公開方法

Class<?> classObj = ProgrameMonkey.class;
Method[] methods = classObj.getMethods();

獲取當前類以及接口的所有公開方法

Class<?> classObj = ProgrameMonkey.class;
Method[] declaredMethods = classObj.getDeclaredMethods();

調用方法

觀察一下 ProgrameMonkey 類的定義。其中 setLanguage() 方法是這樣定義的

public class ProgrameMonkey extends Person implements ICompany {

    // ...
    
    public void setLanguage(String language) {
        this.language = language;
    }

    // ...
}

先傳入方法名和方法簽名,獲取到一個表示 setLanguage 方法的 Method 對象

Method setLanguageMethod = classObj.getMethod("setLanguage", String.class);

然后傳入 ProgrameMonkey 的實例和參數,得到運行結果。因為 setLanguage() 這個方法沒有返回值,所以 result 為 null

Object result = setLanguageMethod.invoke(classObj, "JavaScript");

運行 getLanguage() 方法,觀察一下是否真的改變了屬性的值

System.out.println(programeMonkey.getLanguage());

打印結果

JavaScript

可以看到,屬性 language 的值變了

調用私有方法

ProgrameMonkey 類有一個私有方法

public class ProgrameMonkey extends Person implements ICompany {

    // ...

    private int getSalaryPerMonth() {
        return 123456;
    }
    
    // ...
}

如果按照一般的寫法來寫

Method getSalaryPerMonthMethod = classObj.getMethod("getSalaryPerMonth");
Object result = getSalaryPerMonthMethod.invoke(classObj);

會拋出異常

java.lang.IllegalAccessException: Class com.okada.filemanager.ReflectDemo can not access a member of class com.okada.reflect.ProgrameMonkey with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.checkAccess(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.okada.reflect.ReflectDemo.main(ReflectDemo.java:13)

應該調用 getDeclaredMethod() 方法,否則會拋出 java.lang.NoSuchMethodException 異常。因為 getMethod() 獲取的是公開方法

Method getSalaryPerMonthMethod = programeMonkey.getClass().getDeclaredMethod("getSalaryPerMonth");

另外還要加一句話 getSalaryPerMonthMethod.setAccessible(true);

Method getSalaryPerMonthMethod = classObj.getMethod("getSalaryPerMonth");
getSalaryPerMonthMethod.setAccessible(true);
Object result = getSalaryPerMonthMethod.invoke(classObj);

獲取方法返回值類型

Class<?> returnType = getSalaryPerMonthMethod.getReturnType();

判斷訪問修飾符是否為 private

Modifier.isPrivate(getSalaryPerMonthMethod.getModifiers())

Field

獲取屬性

  • 獲取所有當前類以及父類和接口中用 public 修飾的屬性
ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
Field[] fields = programeMonkey.getClass().getFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

因為只有 age 屬性是用 public 修飾的,所以打印結果為

age
  • 獲取當前類所有的屬性,不管是什么修飾符修飾的
ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
Field[] fields = programeMonkey.getClass().getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

可以看到 ProgrameMonkey 用 private 修飾的屬性都拿到了

language
company
  • 獲取屬性的值
ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
Field[] fields = programeMonkey.getClass().getDeclaredFields();
for (Field field : fields) {
    try {
        field.setAccessible(true); // 別忘了這句話
        System.out.println(field.get(programeMonkey));
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

打印結果

C#
BBK

還可以獲取指定屬性的值

try {
    ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
    Field ageField = programeMonkey.getClass().getField("age");
    System.out.println(ageField.getInt(programeMonkey));
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (SecurityException e) {
    e.printStackTrace();
} catch (IllegalArgumentException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

打印結果

18

設置屬性值

try {
    ProgrameMonkey programeMonkey = new ProgrameMonkey("小明", "男", 18);
    Field ageField = programeMonkey.getClass().getField("age");
    
    System.out.println("before age=" + ageField.getInt(programeMonkey));
    ageField.setInt(programeMonkey, 10086);
    System.out.println("after age=" + ageField.getInt(programeMonkey));
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (SecurityException e) {
    e.printStackTrace();
} catch (IllegalArgumentException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

打印結果

before age=18
after age=10086

反射可以修改 final 修飾的常量的值嗎?

編譯器會對代碼進行優化

來看一個例子

public class Config {

    public final int CONSTANT_VARIABLE = 9527;
}

public class ReflectDemo {

    public static void main(String[] args) {
        System.out.println(new Config().CONSTANT_VARIABLE);
    }
}

在編譯 .java 文件得到 .class 文件的過程中,編譯器會對代碼進行優化。

來實驗一下就知道了。首先使用 Eclipse 導出 jar 包,然后使用 jd-gui 工具打開可以看到反編譯之后得到的 .java 文件

public class Config
{
  public final int CONSTANT_VARIABLE = 9527;
}

public class ReflectDemo
{
  public static void main(String[] args)
  {
    new Config().getClass();System.out.println(9527);
  }
}

可以看到 System.out.println(Config.CONSTANT_VARIABLE); 被編譯器優化成了 System.out.println(9527);Config 類沒有被引用到。這些都是編譯器對代碼進行優化的結果。

所以即使使用反射把 CONSTANT_VARIABLE 的值給改了,依然不能改變 System.out.println(9527); 的結果,這沒有意義。

如果我一定要改呢?

那只能修改源碼了,換一種代碼的寫法,不讓編譯器對代碼進行優化。剛才的寫法,常量是在聲明的時候同時賦值,現在改成常量在構造器里賦值

public class Config {

    public final int CONSTANT_VARIABLE;

    public Config() {
        CONSTANT_VARIABLE = 9527;
    }
}

public class ReflectDemo {

    public static void main(String[] args) {
        System.out.println(new Config().CONSTANT_VARIABLE);
    }
}

反編譯之后的代碼

public class Config
{
  public final int CONSTANT_VARIABLE;
  
  public Config()
  {
    this.CONSTANT_VARIABLE = 9527;
  }
}

public class ReflectDemo {

    public static void main(String[] args) {
        System.out.println(new Config().CONSTANT_VARIABLE);
    }
}

可以看到,編譯器沒有對這兩個類進行優化,因為根本無法優化?,F在使用反射來改變一下 CONSTANT_VARIABLE 的值

public class ReflectDemo {

    public static void main(String[] args) {
        try {
            Config cfg = new Config();
            Field finalField = cfg.getClass().getDeclaredField("CONSTANT_VARIABLE");
            finalField.setAccessible(true);
            System.out.println("before modify, CONSTANT_VARIABLE=" + finalField.getInt(cfg));
            finalField.setInt(cfg, 1234);
            System.out.println("after modify, CONSTANT_VARIABLE=" + finalField.getInt(cfg));
            System.out.println("actually, CONSTANT_VARIABLE=" + cfg.CONSTANT_VARIABLE);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

打印結果

before modify, CONSTANT_VARIABLE=9527
after modify, CONSTANT_VARIABLE=1234
actually, CONSTANT_VARIABLE=1234

可以看到,在修改之前,CONSTANT_VARIABLE 的值是 9527,接著使用反射把 CONSTANT_VARIABLE 的值改成 1234。最后調用 cfg.CONSTANT_VARIABLE 驗證一下,是否修改成功,發現修改成功了。

所以,如果要該常量的值,只能在代碼的寫法上進行變通,避免編譯器的優化。

開發中的實際應用

獲取注解信息

在 Android 應用開發的過程中,經常需要寫 findViewById()。這樣的重復工作可以交給注解來完成。

首先定義一個注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    int value();
}

在 Activity 中使用注解

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.tv)
    TextView mTextView;
}

然后在 onCreate() 方法中解析注解,去幫我們執行 findViewById 的操作

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.tv)
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Views.inject(this);  // 解析注解
    }
}

解析注解的過程如下

public class Views {

    public static void inject(Activity activity) {
        Class<? extends Activity> activityClass = activity.getClass();
        Field[] fields = activityClass.getDeclaredFields();
        for (Field field : fields) {
            InjectView injectViewAnnotation = field.getAnnotation(InjectView.class);
            if (injectViewAnnotation != null) {
                View view = activity.findViewById(injectViewAnnotation.value());
                try {
                    field.setAccessible(true);
                    field.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

利用反射,獲取實例的注解信息,然后獲取到注解的值,最后去調用 findViewById() 方法。

工作原理

當我們編寫完一個 Java 項目之后,所有的 Java 文件都會被編譯成一個.class 文件,這些 Class 對象承載了這個類型的父類、接口、構造函數、方法、屬性等原始信息,這些 class 文件在程序運行時會被 ClassLoader 加載到虛擬機中。當一個類被加載以后,Java 虛擬機就會在內存中自動產生一個 Class 對象。我們通過 new 的形式創建對象實際上就是通過這些 Class 來創建,只是這個過程對于我們是不透明的而已。

所以,只要拿到了 Class 對象,就可以做一系列的反射操作

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容