什么是反射
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
對象,就可以做一系列的反射操作