Java 反射

 

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

通过反射获取类字段和方法的信息.

通过getFieldsgetMethods 方法会获取对于能公共访问的字段或方法. 通过getDeclaredFieldsgetDeclaredMethods 方法会获取能该类所声明的字段或者方法(写在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 显示如下 enter description here

通过反射调用对象的方法或者字段

这里要注意一点, 如果该方法或字段是私有的, 那么必须通过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());
    }

enter description here

尝试修改常量字段

这里尝试修改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());
    }

enter description here

果然是不成功的, 如果其他类或者方法在编码过程中直接或者间接引用了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 该常量. 那么这样的话. 就没有办法了么? 嘿嘿嘿, 这怎么难得倒机智的程序员们呢. 既然他编译期进行优化, 那么就防止他优化.

  1. 通过运算语句防止优化
    //修改赋值语句为这个.
    private final String DESC = null == null ? "你在这站着等我下, 我去买几个橘子." : null;
    

    通过反编译, 我们看下结果.

	private final String DESC = null == null ? "你在这站着等我下, 我去买几个橘子." : null;
    public String getDesc() {
        return this.DESC;
    }

哈哈哈, 成功解决. 这次不直接返回常量, 而是一个间接引用.

  1. 在构造方法中进行常量的赋值.
    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中已经变为间接引用, 目的已经达到.

我们防止了编译期优化之后, 再尝试下, 是否能通过反射进行修改常量. enter description here

成功!

这里说下, 我所说的直接引用和间接引用, 以免有的人懵.

JVM方法区中是存在一个常量池的, 常量池中存储着一些我们写在代码中的常量, 如”哈哈哈” , “不了, 不爱吃橘子” 1111, 222等各种基础常量.

Class文件加载过程中, 会将class文件中对于本文件中常量的引用, 转化到常量池中的引用, 如果方法返回值直接指向了常量池, 那么我们修改DESC这个变量显然是无济于事的.

所以指向DESC, 再指向常量池 –> 间接引用, 直接指向常量池 –>直接引用. (只针对此篇文章, 需根据情况理解)