Java中的反射技术

反射技术是java的核心技术之一,虽然我们日常开发中,基本上可能用的并不多,但是它同样也是必学的。因为很多框架的设计其实都是有利用反射机制的,这意味着反射是我们向前迈进的一个重要技术。

 

一、反射是什么?

首先我们看一段普通的调用代码

先创建一个平平无奇的Person类,有一个平平无奇的work方法

package com.lzh.reflect;


public class Person {
    public void work(String content) {
        System.out.println(content);
    }
}

再创建个Test类进行调用

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.work("编码中");
    }
}

结果不用贴了,只是将字符串“编码中”,输出到了控制台。

 

然后现在我们利用反射机制,做相同的调用操作:

public class Test {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, ClassNotFoundException {
        Class clz = Class.forName("com.lzh.reflect.Person");
        Method method = clz.getMethod("work",String.class);
        Constructor constructor = clz.getConstructor();
        Object object = constructor.newInstance();
        method.invoke(object,"编码中");
    }
}

??

别看这一坨代码比较臃肿复杂,但是它的执行效果和前面的普通调用其实是一样的。只是它是通过反射去调用的,最明显的区别就是,反射在调用同一个类的同一个方法时,并没有指定类。听起来有点绕,但是如果你的Person和Test两个文件在不同的包下,就可以看出来,普通的调用必须import com.lzh.reflect.Person;,导入Person类。但如果是反射,就不需要导入具体的包,而是通过运行时分析参数里的类路径字符串判断出是哪个类。

站在编码者的角度,即便用了反射,我们也知道是用了哪个类,调用了哪个方法,但是对于jvm来说,它们不运行到这一步是不会知道的。在编译时,类的路径名、方法名对它来说只是个字符串参数而已。

通过反射我们甚至可以通过修改配置或传特定的参数,无需重启服务器,就能更改服务器内部的调用逻辑,所以这点也是一个安全隐患,在使用反射时一定要小心使用外部输入的参数。

 

总结:普通调用在编译后,就知道了要加载哪个类,调用这个类的哪个方法。但是反射是在运行时才知道要操作的类是什么,并且在运行时获取类的某个构造方法构建对象,然后调用类的某个方法。

 

 

二、怎么使用反射

1.获取Class

首先我们需要通过反射获取一个类的对象

①Class.forName(类的全路径) 静态方法

第一种方式,就是我们前面例子用的方法了。使用Class.forName(类的全路径) 指定类,这种方式我们需要知道类的全路径名,如果路径有误加载不到类,会抛出ClassNotFoundException异常。

Class clz = Class.forName("com.lzh.reflect.Person");
②.class方法

这种方法是需要导入类的,指定了类名,看起来好像有点脱裤子放屁没啥用,因为反射不就是因为不能确定类才用反射吗。但是如果我们重点关注的是反射调用哪个方法或哪个构造函数,而Class其实是已经确定的,所以并不需要在Class上费时间时,是可以用到这种方式的。

Class clz = Person.class;
③对象的getClass()方法

这种方式就更简单了,直接通过对象获取类,比方式②还要具体,这次连构造方法都无需关注了。同样,可能在我们只关注method的时候可以用上。

Person person = new Person();
Class clz1 = person.getClass();
④基本类型的Type属性

如果是基本类,我们可以通过对应包装类的TYPE属性,获取它基本类的Class,每个基本类型都有这个TYPE属性。

Class byteClass = Byte.TYPE;
Class shortClass = Short.TYPE;
Class integerClass = Integer.TYPE;
Class longClass = Long.TYPE;
Class charClass = Character.TYPE;
Class floatClass = Float.TYPE;
Class doubleClass = Double.TYPE;
Class boolClass = Boolean.TYPE;
或简便写成
Class byteClass = byte.class;
Class shortClass = short.class;
Class integerClass = int.class;
Class longClass = long.class;
Class charClass = char.class;
Class floatClass = float.class;
Class doubleClass = double.class;
Class boolClass = boolean.class;

也可以简便的写成int.class、byte.class这样,效果是一样的,通过反编译可以看到int.class最后实际上也是Integer.TYPE。

但是注意,这并不等于Integer.class,基本类型的Class类型和包装类的Class类型是两码事,它们并不是一个东西

⑤类Class

我们也可以在获取了子类Class对象后,通过getSuperclass()获取它的父类Class。

如下,Student的父类是Person:

Student student = new Student();
Class slz = student.getClass();
Class superclass = slz.getSuperclass();

 

2.获取对象

获取了Class后,我们自然需要通过某个构造方法获取这个类的对象

①通过Class对象的newInstance()方法

直接通过类获取实例,但是原理是调用无参构造方法,所以如果这个类没有无参构造方法,会抛出InstantiationException异常。

Class clz = Person.class;
Person person = (Person) clz.newInstance();
②通过Constructor 对象的 newInstance() 方法

如果我们想调用指定的构造方法,就得用这种方式了。先给Person类随便加一个有参的构造方法:

public class Person {
    //有参构造方法
    public Person(String name) {
        System.out.println(name);
    }
    public void work(String content) {
        System.out.println(content);
    }
}

然后通过clz.getConstructor(String.class) 获取指定参数的构造器(如果是获取无参构造就不传参数),这里参数也是类的Class,最后通过构造器的newInstance()方法调用构造方法,同时这里需要传入前面声明的参数:

Class clz = Person.class;
Constructor constructor = clz.getConstructor(String.class);
Person person = (Person) constructor.newInstance("123");

注意这里的newInstance()方法是构造器Constructor对象中的方法,和前面的Class中的不是一个方法,所以也会抛出更多种类的异常。

③私有构造方法

如果这个构造方法是私有的呢,给工具人Person类加一个私有的构造方法:

    private Person(Integer age) {
        System.out.println(age);
    }

如果还是用前面的getConstructor(Integer.class);就会发现抛出了NoSuchMethodException异常,找不到这个私有的构造方法,这是因为普通的getConstructor只能获取到public级别的构造方法

这时候我们只需要将getConstructor()替换成getDeclaredConstructor()方法即可,其实就是加了个关键字Declared,它可以读取到所有已声明的构造方法,这样即便是私有的也可以找到。另外还需要额外设置一下setAccessible为true,关闭反射对象的访问检查,不然也是无法访问私有构造方法的。

Class clz = Person.class;
Constructor constructor = clz.getDeclaredConstructor(Integer.class);
constructor.setAccessible(true);
Person person = (Person) constructor.newInstance(18);

 

3.获取成员变量

首先还是掏出工具人Person类,给它上两个变量,其中一个设为了私有。同时为了看结果,我重新生成了toString方法。

public String name;
private Integer age;


@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
①通过getField()方法获取成员变量

通过getField()获取指定的成员变量,可以通过getName获取成员变量的名字,getType获取成员变量的类型

Class clz = Person.class;
Field field = clz.getField("name");
System.out.println(fields.getName());//获取名字
System.out.println(fields.getType());//获取类型

如果要获取值,则需要传入一个对象才行,然后直接调用get方法:

//初始化一个Person对象,并赋给成员变量一个值
Person person = new Person();
person.name = "张三";


//通过反射取出这个成员变量的值
Class clz = person.getClass();
Field field = clz.getField("name");//指定field是name变量
System.out.println(field.get(person));//获取传入对象person的name成员变量值
②通过field.set()为对象设置成员变量值

和get的使用方法一样,set也要传入一个Person对象,不然它怎么知道是给哪个对象设置值呢?这里我们试着用反射构造一个对象,再反射set变量属性后反射get出来:

Class clz = Person.class;    //声明Class
Constructor clzConstructors = clz.getConstructor();    //获取默认的无参构造器
Object person = clz.newInstance();//初始化person对象


//获取成员属性,并set值进去
Field field = clz.getField("name");
field.set(person, "马德");
//输出对象的属性
System.out.println(field.get(person));

有人可能觉得你用了同一个Field对象set完了再get,当然get的到啊。实际上就算你在get的步骤前重新初始化一个Field对象去get,结果也是一样的。field并不会绑定在某一个对象上。

③私有成员变量

和构造器一样,只需要加上Declared,用getDeclaredField() ,就可以获取私有级别的成员变量。记住,没有加Declared的方法都是只能访问public级别的,不管是构造器还是成员变量还是方法。

并且也都需要setAccessible(true);关闭访问级别的检查:

Field field = clz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 18);
System.out.println(field.get(person));

 

4.获取和调用方法

最后终于到核心了,利用反射调用方法

①通过getMethod()获取方法,invoke调用方法

这里我偷了个懒,直接把最前面文章开头的例子拿过来了,方法还是普通的work方法

    public void work(String content) {
        System.out.println(content);
    }

这次,我们应该能看懂这个调用了

Class clz = Class.forName("com.lzh.reflect.Person");
Method method = clz.getMethod("work",String.class);    //获取work(String)方法
Constructor constructor = clz.getConstructor();    //获取构造器
Object object = constructor.newInstance();    //获取对象实例
method.invoke(object,"编码中");    //调用object对象的work方法,并传入参数

首先通过clz.getMethod("work",String.class); 获取了work方法,第二个包括以后的参数代表work方法的参数,有几个就要填几个,不然如果work方法有多个重载,反射无法找到具体的方法。

最后调用时是用method.invoke(object,"编码中"); 方法,第一个参数是要执行哪个对象的work方法,和field对象的getset方法类似,第二个包括以后的参数就是work方法所需的参数了,数量要对上,不然会抛出IllegalArgumentException异常。

②调用私有方法

估计大家都猜到怎么调用私有了,getDeclaredMethod() 可以访问到所有方法,包括私有方法。记得如果没加Declared,就只能查到public级别的。所以一般我们用getDeclaredMethod就行,稳妥。

并且要记得method.setAccessible(true)。

然后我们整个最完整的反射,没有import导入Person类,完整的调用一次方法

Person类:

public class Person {
   
    public String name;
    private Integer age;    //私有属性年龄
   
    public Person(String name) {    //公共有参构造方法
        this.name = name;
    }
   
    private String play(String game) {        //私有有参方法,且用到了成员变量
        return this.name+"("+this.age+") "+"play ♂ " + game;
    }


}

调用的测试方法:

//通过反射获取Class
Class clz = Class.forName("com.lzh.reflect.Person");


//获取有参构造器,那个初始化同时会设置name的构造器
Constructor constructor = clz.getDeclaredConstructor(String.class);


//获取实例,并初始化了name属性
Object person = constructor.newInstance("比利");


//手动获取age成员属性,并设置值
Field field = clz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 28);


//调用方法
Method method = clz.getDeclaredMethod("play", String.class);
method.setAccessible(true);
Object invoke = method.invoke(person,"摔跤");
System.out.println(invoke);

这次放一次输出结果:

比利(28) play ♂ 摔跤

 

*5.一些其他的方法

再简单说明和反射相关的几个其他方法,非必读,可选择性跳过。

①获取构造器、方法、变量加s

Class对象中,我们已经知道了有getConstructor()和getDeclaredConstructor()获取构造器,getMethod()和getDeclaredMethod()是获取方法,getField()和getDeclaredField()是获取属性。

但是如果在这些方法名的尾部加个s,就代表获取全部的构造器/方法/属性,

返回类型是:Constructor或Method或Field类型的数组

Constructor[] clzConstructors = clz.getConstructors();
Constructor[] declaredConstructors = clz.getDeclaredConstructors();


Field[] fields = clz.getFields();


Method[] methods = clz.getMethods();
②getModifiers()

Constructor、Method、Field对象都有方法getModifiers(),返回的是一个整数值,它代表当前的方法或属性前面的修饰符相加得到的值:无任何修饰符是0 , public是1 ,private是2 ,protected是4,static是8 ,final是16。例如一个方法前面的修饰符是public static final ,那么它的值就是相加得到25。

返回类型是:int

这个方法虽然只返回一个数字,但是通过计算是一定能准确的得到修饰符的,因为都是2的倍数,所以每个返回的数字只会有计算出一种结果。

而且我们也没必要计算,反射有提供Modifier.toString() 方法,帮我们把这个数字转换为修饰符:

System.out.println("修饰符: "+Modifier.toString(method.getModifiers()));
③getParameterTypes()

Constructor、Method对象的方法getParameterTypes(),返回当前方法的参数列表,是数组格式的Class列表。

返回类型是:Class[]

System.out.println(Arrays.toString(work.getParameterTypes()));

 

 

三、总结

至此,只要认真的看完并自己尝试后,我们对于反射就算没入门,也至少会用了。

我也是工作了一年后才开始学习的反射,因为工作中没怎么用过,呃,应该说就没用过。

但是当我们查看一些牛逼框架代码的源码时,就会发现里面很多地方有用到反射,就拿我们耳熟能详的Spring来说,它的核心IOC和AOP功能也是依靠了反射的。所以就算只是为了看源码,为了应付面试官,我们也应当学会反射,学了反射大家都是人上人。

 

 

 

参考资料:

大白话说Java反射:入门、使用、原理

https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html

Java反射技术详解

https://blog.csdn.net/huangliniqng/article/details/88554510

你可能感兴趣的:(Java中的反射技术)