到底什么是反射?反射究竟能用来干什么?

反射,顾名思义,它是一种逆向的操作。就好像人在照镜子的时候,正是由于光的反射,才能看到镜子中的自己。而在Java中,反射功能就好比是一面镜子,通过它,我们可以在程序运行过程中看到Class以及对象的相关信息。

在以往的经验中,当我们需要完成某些操作,往往是在编译之前完成,比如根据创建对象、读取属性、设置属性;我们把这些程序编写完之后编译器会将之编译为Class文件,然后直接在虚拟机中运行就可以了,大部分情况确实是这样,这也是为什么Java是"静态语言"。

但是我们却可以通过反射来完成在动态语言中才能做到的一些操作,比如首先第一步,通过反射获取某个.class文件的结构信息。

示例如下:

/**
 * 蝙蝠侠
 */
public class Batman {
    public Batman(String name,int age,String power){
        this.name = name;
        this.age = age;
        this.power = power;
    }
    //蝙蝠侠的名字
    private String name;
    //蝙蝠侠的年龄
    private int age;
    //蝙蝠侠的超能力
    private String power;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getPower() {
        return power;
    }

    public void setPower(String power) {
        this.power = power;
    }
    
    /** 跟小猫谈恋爱 */
    private void beInLove(){
        System.out.println("蝙蝠侠正在跟小猫谈恋爱");
    }

    /** 教训小丑 */
    public void work(){
        System.out.println("蝙蝠侠正在教训小丑");
    }

    private void workWithGordon(String name){
        System.out.printf("蝙蝠侠正在和%s一起教训小丑",name);
    }
}
复制代码
//测试类
public class ReflectTest {
    public static void main(String[] args) {
        Class cls = Batman.class;
        //1.获取class中的所有属性,包括全局属性和局部属性
        Field[] fields = cls.getDeclaredFields();
        System.out.println("属性:");
        for (Field f : fields) {
            System.out.println(f);
        }
        System.out.println("方法:");
        //获取class中的所有方法
        Method[] mets = cls.getDeclaredMethods();
        for (Method met : mets) {
            System.out.println(met);
        }
        System.out.println("构造器:");
        //获取class中的所有构造器
        Constructor[] cons = cls.getDeclaredConstructors();
        for (Constructor c : cons) {
            System.out.println(c);
        }
    }
}
复制代码

运行结果:
属性:
private java.lang.String day2.demo2.Batman.name private int day2.demo2.Batman.age private java.lang.String day2.demo2.Batman.power
方法:
public int day2.demo2.Batman.getAge() public void day2.demo2.Batman.setAge(int) public java.lang.String day2.demo2.Batman.getPower() public void day2.demo2.Batman.setPower(java.lang.String) public java.lang.String day2.demo2.Batman.getName() public void day2.demo2.Batman.setName(java.lang.String)
构造器:
public day2.demo2.Batman(java.lang.String,int,java.lang.String)

java程序在运行时,虚拟机在加载类时,会为这个类创建一个Class对象,用来表示这个类的信息。可以通过类名.class、Class.forName("类名")、Object.getClass等方式获取到一个Class对象,这个对象记录了类的信息,通过它可以逆向获取类的结构。

在示例中,通过Class对象中方法的调用,获取了Batman类中的所有属性、方法、构造器,但是反射的功能远不止于此,比如通过上述三个方法获取到的Field、Method、Constructor数组对象完成进一步的操作:

public class ReflectTest {
    public static void main(String[] args) {
        Class cls = Batman.class;
        Field[] fields = cls.getDeclaredFields();
        System.out.println("属性:");
        System.out.println("访问修饰符:" + Modifier.toString(fields[0].getModifiers()));
        System.out.println("是否静态的:" + Modifier.isStatic(fields[0].getModifiers()));
        System.out.println("是否为public:" + Modifier.isPublic(fields[0].getModifiers()));
        System.out.println("是否常量:" + Modifier.isFinal(fields[0].getModifiers()));
        System.out.println("方法:");
        Method[] mets = cls.getDeclaredMethods();
        System.out.println("是否为本地方法:" + Modifier.isNative(mets[0].getModifiers()));
        System.out.println("是否为抽象方法:" + Modifier.isAbstract(mets[0].getModifiers()));
        System.out.println("是否为接口:" + Modifier.isInterface(mets[0].getModifiers()));
        System.out.println("是否线程同步:" + Modifier.isSynchronized(mets[0].getModifiers()));
        System.out.println("构造器:");
        Constructor[] cons = cls.getDeclaredConstructors();
        System.out.println("是否公有:"+Modifier.isPublic(cons[0].getModifiers()));
        /**
         * ...
         * */
    }
}
复制代码

以上这些都是直接对Class类的操作,其实java反射也同样支持对运行中的对象的操作,甚至可以修改对象中属性的值。

示例代码:

public class ReflectTest {
    public static void main(String[] args) throws IllegalAccessException {
        Batman batman = new Batman("布鲁斯韦恩",27,"有钱");
        Class cls = batman.getClass();
        Field[] fields = cls.getDeclaredFields();
        /**获取第一个属性name的值,由于是private属性,
           所以会报IllegalAccessException异常,很显然是与权限有关*/
        try {
            var name = fields[0].get(batman);
            System.out.println(name);
        }catch (IllegalAccessException e){
            //这里通过一个方法设置可访问对象的可访问标志
            fields[0].setAccessible(true);
            var name = fields[0].get(batman);
            System.out.println(name);
        }
        //但是这里设置的只是数组中第一个属性的访问权限,下面这句话依然会报错
        try {
            var name = fields[1].get(batman);
            System.out.println(name);
        }catch (IllegalAccessException e){
            //所以通过下面的方法对整个数组对象的访问权限进行设置
            AccessibleObject.setAccessible(fields,true);
            var age = fields[1].get(batman);
            var power = fields[2].get(batman);
            System.out.println(age);
            System.out.println(power);
        }
        //修改fields[2]的值
        fields[2].set(batman,"哥谭首富");
        System.out.println("超能力:"+batman.getPower());
    }
}
复制代码

运行结果:
布鲁斯韦恩
27
有钱
超能力:哥谭首富

通过调用对象的getClass()方法获取这个类唯一的Class对象,再通过获取到field对象的get(obj)方法获取到这个field的值(当然如果属性是私有的,还需要使用setAccessible方法设置访问标志),并且不仅可以获取,还能通过其set(obj,val)方法重新设置这个属性的值。而这一切都是在程序运行期间完成的,成功的通过反射修改了对象中的属性。

至此,已经实现了通过反射来查看类的信息、对象的属性以及设置对象的属性。那么如何通过反射来调用方法以及构造器呢?

示例代码:

public class ReflectTest {
    public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, InstantiationException {
        Class cls = Batman.class;
        //通过反射调用构造器创建蝙蝠侠对象
        Batman batman = (Batman) cls.getDeclaredConstructor(String.class,int.class,String.class).newInstance("蝙蝠侠", 27, "有钱");
        Method method = cls.getDeclaredMethod("beInLove");
        //因为beInLove()方法是私有的,所以需要设置以下权限
        method.setAccessible(true);
        method.invoke(batman);
        Method work = cls.getDeclaredMethod("work");
        //work方法不是private的,不需要设置权限
        work.invoke(batman);

        //调用带参数的方法
        Method workWithGordon = cls.getDeclaredMethod("workWithGordon", String.class);
        //私有方法依然要设置权限
        workWithGordon.setAccessible(true);
        workWithGordon.invoke(batman,"Gordon");
    }
}
复制代码

至此,就完成了方法及构造方法的调用。需要注意的是,若调用了一个带返回值的方法,如果返回值类型是基本类型,invoke方法会返回其包装类型,如int返回Integer、double返回Double。

另外,java.lang.reflect包中还提供了一个很好用的类Array,ArrayList中的数组扩容就使用到了这个类。

示例代码:

public class ReflectTest {
    public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, InstantiationException {
        //假如我想创建一个数组
        String[] strs = {"bruce","jack","jerry"};
        strs = (String[]) CopyOf(strs,10);
    }

    //现在我想写一个方法来为泛型数组扩容
    public static Object[] CopyOf(Object[] obj,int nlength){
        var newArray = new Object[nlength];
        System.arraycopy(obj,0,newArray,0,Math.min(obj.length,nlength));
        return newArray;
    }
}
复制代码

运行结果:Exception in thread "main" java.lang.ClassCastException

这段代码看起来好像没有问题,通过Object超类接收任意类型的数组。但是有一个细节问题,当创建一个数组然后将其转为Object[],再把它从Object[]转回来是没有问题的,但是如果直接创建一个Object[]转成目标类型的数组是会出错的。所以上述代码无法完成泛型数组的扩容。

现在对代码做一些改进,示例代码:

public class ReflectTest {
    public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, InstantiationException {
        //假如我想创建一个数组
        String[] strs = {"bruce","jack","jerry"};
        strs = (String[]) CopyOf(strs,10);
        System.out.println(strs.length);
    }

    //现在我想写一个方法来为泛型数组扩容
    public static Object CopyOf(Object obj,int nlength){
        Class cls = obj.getClass();
        if(!cls.isArray()) return null;
        //获取数组的类型
        Class type = cls.getComponentType();
        //获取数组的长度
        int length = Array.getLength(obj);
        //通过Array.newInstance创建一个泛型数组,类型通过参数指定
        Object newArray = Array.newInstance(type,nlength);
        System.arraycopy(obj,0,newArray,0,Math.min(length,nlength));
        return newArray;
    }
}
复制代码

运行结果:10

这次程序成功运行,并且成功为数组扩容。最主要的原因是代码中的关键方法,Array类的静态方法newInstance,这个方法能够返回一个有给定类型,给定大小的新数组,而不是一个简单的Object[]。

总结:反射机制可以在运行时查看、操作字段和方法。但是不应该滥用反射,因为反射在编译阶段无法查找出错误,如果存在问题,往往到了运行时才会发现。JVM无法对反射的相关代码做优化,所以效率相对低。并且反射可能导致程序不安全。

猜你喜欢

转载自juejin.im/post/7228967103349080120