在Java的类加载过程中会在方法区创建一个描述该类的对象, 通过调用这个对象, 我们直接操作在编译之后的类的信息, 这就是所谓的Java 反射.
详情见Java Class类的DOC:https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html
接下来我们来编码, 使用一些方法, 来感受反射的魅力.
这是待会我们要操作的Father类
public class Father {
public String name;
private int age;
private long id;
private final String DESC = "你在这站着等我下, 我去买几个橘子.";
// private final String DESC;
//
// public Father() {
// this.DESC = "你在这站着等我下, 我去买几个橘子.";
// }
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public long getId() {
return id;
}
//注意这里是私有的
private void setId(long id) {
this.id = id;
}
public String getDesc() {
return DESC;
}
}
通过反射获取类字段和方法的信息.
通过getFields
或 getMethods
方法会获取对于能公共访问的字段或方法.
通过getDeclaredFields
或 getDeclaredMethods
方法会获取能该类所声明的字段或者方法(写在Father.java 或者 说被编译在Father.class中的)
private static void getClassInfoByReflect()throws Exception{
Class father = Father.class;
Field[] fields = father.getFields();
System.out.println("这是Father类能公共访问的字段: : ");
for (Field field : fields){
System.out.println(field.toString());
}
Field[] declaredFields = father.getDeclaredFields();
System.out.println("这是Father类单独声明的所有字段: ");
for (Field field : declaredFields){
System.out.println(field.toString());
}
Method[] methods = father.getMethods();
Method[] declaredMethods = father.getDeclaredMethods();
Method setAgeMethod = father.getMethod("setAge", int.class);
System.out.println("这是Father类能公共访问的函数(包括从Object父类继承来的):");
for (Method m : methods){
System.out.println(m);
}
System.out.println("这是Father类单独声明的所有函数:");
for (Method m : declaredMethods){
System.out.println(m);
}
}
运行这个方法, terminal 显示如下
通过反射调用对象的方法或者字段
这里要注意一点, 如果该方法或字段是私有的, 那么必须通过setAccessible
设置访问权限为true, 才能进行访问或修改.
private static void invokeMethodByReflect()throws Exception{
Father father = new Father();
Class clazzOfFather = Father.class;
Method setIdMethod = clazzOfFather.getDeclaredMethod("setId", long.class);
//通过直接修改字段也可以
// Field idField = clazzOfFather.getDeclaredField("id");
// idField.setAccessible(true);
// idField.set(father, 50);
System.out.println("之前的father id : " + father.getId());
if (setIdMethod != null){
setIdMethod.setAccessible(true);
setIdMethod.invoke(father, 50);
}
System.out.println("修改过的father id : " + father.getId());
}
尝试修改常量字段
这里尝试修改final关键字修饰过的字段, 我觉得应该是不会成功的, 因为被修改的String类型常量, 早就在编译期就被优化成直接引用.
private static void tryChangeFinalFiled()throws Exception{
Father father = new Father();
Class clazzOfFather = Father.class;
Field descField = clazzOfFather.getDeclaredField("DESC");
System.out.println("修改之前的DESC : " + father.getDesc());
if (descField != null){
descField.setAccessible(true);
descField.set(father, "不了, 不爱吃橘子");
}
System.out.println("修改之后的DESC : " + father.getDesc());
}
果然是不成功的, 如果其他类或者方法在编码过程中直接或者间接引用了DESC, 就会被优化为对于”你在这站着等我下, 我去买几个橘子.”的直接引用.
我们来看下反编译的Father.Java文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.reflect;
public class Father {
public String name;
private int age;
private long id;
private final String DESC = "你在这站着等我下, 我去买几个橘子.";
public Father() {
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
public long getId() {
return this.id;
}
private void setId(long id) {
this.id = id;
}
public String getDesc() {
return "你在这站着等我下, 我去买几个橘子.";
}
}
其中的getDesc
方法直接return 该常量.
那么这样的话. 就没有办法了么?
嘿嘿嘿, 这怎么难得倒机智的程序员们呢.
既然他编译期进行优化, 那么就防止他优化.
- 通过运算语句防止优化
//修改赋值语句为这个. private final String DESC = null == null ? "你在这站着等我下, 我去买几个橘子." : null;
通过反编译, 我们看下结果.
private final String DESC = null == null ? "你在这站着等我下, 我去买几个橘子." : null;
public String getDesc() {
return this.DESC;
}
哈哈哈, 成功解决. 这次不直接返回常量, 而是一个间接引用.
- 在构造方法中进行常量的赋值.
private final String DESC;
public Father() {
this.DESC = "你在这站着等我下, 我去买几个橘子.";
}
看下反编译的结果
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.reflect;
public class Father {
public String name;
private int age;
private long id;
private final String DESC = "你在这站着等我下, 我去买几个橘子.";
public Father() {
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
public long getId() {
return this.id;
}
private void setId(long id) {
this.id = id;
}
public String getDesc() {
return this.DESC;
}
}
这次的结果好像不是我们想象的那样, 并没有在构造函数中进行初始化常量, 但是getDesc
中已经变为间接引用, 目的已经达到.
我们防止了编译期优化之后, 再尝试下, 是否能通过反射进行修改常量.
成功!
这里说下, 我所说的直接引用和间接引用, 以免有的人懵.
JVM方法区中是存在一个常量池的, 常量池中存储着一些我们写在代码中的常量, 如”哈哈哈” , “不了, 不爱吃橘子” 1111, 222等各种基础常量.
Class文件加载过程中, 会将class文件中对于本文件中常量的引用, 转化到常量池中的引用, 如果方法返回值直接指向了常量池, 那么我们修改DESC这个变量显然是无济于事的.
所以指向DESC, 再指向常量池 –> 间接引用, 直接指向常量池 –>直接引用. (只针对此篇文章, 需根据情况理解)