什么是反射
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);
}
}
可以看到,编译器没有对这两个类进行优化,因为根本无法优化。现在使用反射来改变一下 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
对象,就可以做一系列的反射操作