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);
    }
}

可以看到,编译器没有对这两个类进行优化,因为根本无法优化。现在使用反射来改变一下 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阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容