关于Java反射

反射是什么?

反射,是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。对于Java来说,反射就是操作类的Class对象来让我们可以在程序运行时发现和使用类的所有信息。当然如果没有了解Java的类的加载,可能看到这里会有个疑惑,直接New这个对象不就完了,为什么还需要反射?
那么首先我们都知道Java具有平台无关性,而之所以具有平台无关性,是因为Java会使用Javac编译,生成对应类的字节码,也就是.class文件,然后使用对应不同平台的JVM进行解析,转换成特定平台的执行指令。也就是说,我们每使用一个类,Java就会产生一个对应的Class对象。而生成这个class对象,会使用不同的类加载器进行加载。
关于类加载器,建议另外再详细学习一下。这里就不赘述了。我们这里只需要知道,从.java到.class这个过程。而如果我们现在有一个需求,现在我有A类以及B类,我想根据运行进行加载不同的类,那么这个时候,是不是就需要用到反射了。

反射的具体使用

创建测试类Student

public class Student {
    private String name;
    private String sex;
    private int age;
}

创建Class对象

  • 使用对象.getClass()获取
Student student=new Student();
Class class=student.getClass();
  • 根据类名.class来获取
Class class=Student.class;
  • 使用Class.forName()方法获取,其中参数必须为带包名的类路劲
Class class=Class.forName("com.scominfo.demo01.entity.Student");

第一种方法已经实例化对象了,再用反射没必要。第三种方法需要导入包路劲,不太方便,所以一般我们使用第二种方法。

使用反射

当我们获取到Class对象之后,就可以开始对其进行操作了。

获取公有构造方法

使用Class类中的getConstructors()方法获取到所有Public的构造方法列表、

 Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Constructor[] constructors = studentClass.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }

查看打印结果如下:

public com.scominfo.demo01.entity.Student(java.lang.String,java.lang.String,int)
public com.scominfo.demo01.entity.Student()

获取全部构造方法

通过以上方法获取,我们发现没有找到private的构造函数。如果想要获取"public,protected,default,privite"的构造函数,则应该使用getDeclaredConstructors()方法。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Constructor[] constructors = studentClass.getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }

查看打印结果如下:

private com.scominfo.demo01.entity.Student(java.lang.String)
public com.scominfo.demo01.entity.Student(java.lang.String,java.lang.String,int)
public com.scominfo.demo01.entity.Student()

此时发现private的构造方法也一起出现了。

获得具体类型的构造方法

那么如果我们需要获取到具体的某一个构造,就需要通过传入构造方法的入参的类型。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Constructor constructor = studentClass.getConstructor(null);
        Constructor constructor1 = studentClass.getConstructor(String.class, String.class, int.class);
        Constructor declaredConstructor = studentClass.getDeclaredConstructor(String.class);
        System.out.println(constructor);
        System.out.println(constructor1);
        System.out.println(declaredConstructor);

查看打印结果,发现确实按照构造方法的入参类型进行输出了。

public com.scominfo.demo01.entity.Student()
public com.scominfo.demo01.entity.Student(java.lang.String,java.lang.String,int)
private com.scominfo.demo01.entity.Student(java.lang.String)

注意:如果填入了没有对应构造方法入参类型,则会出现NoSuchMethodException错误。

通过构造方法实例化类

在我们获得了Constructor对象后,可以通过其中的newInstance方法进行实例化对象。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Constructor studentconstructor = studentClass.getConstructor(null);
        Constructor studentconstructor1 = studentClass.getConstructor(String.class, String.class,int.class);
        Student student = (Student)studentconstructor.newInstance();
        Student student1=(Student)studentconstructor1.newInstance("ltp","男",25);
        student.setAge(28);
        System.out.println(student);
        System.out.println(student1);

查看打印信息:

Student{name='null', sex='null', age=28}
Student{name='ltp', sex='男', age=25}

获取公有成员变量

首先我们需要将上述实体类进行修改,将年龄改为公有变量public。名字和性别依然为私有。如无特殊说明,以下代码默认已经进行修改。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Field[] fields = studentClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

输出结果如下:

public int com.scominfo.demo01.entity.Student.age

那么我们可以得知,Class对象.getFields()只能获取共有成员变量列表。

获取所有成员变量

获取所有成员变量类似于获取所有构造函数一样,使用getDeclaredFields()方法即可。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Field[] fields = studentClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }

输出结果如下:

private java.lang.String com.scominfo.demo01.entity.Student.name
private java.lang.String com.scominfo.demo01.entity.Student.sex
public int com.scominfo.demo01.entity.Student.age

获取指定成员变量

获得指定的成员变量其实和上面的获取指定的构造方法是一样的,使用getDeclaredField("属性名")即可

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Field field = studentClass.getDeclaredField("name");
        System.out.println(field);

输出结果如下:

private java.lang.String com.scominfo.demo01.entity.Student.name

为成员变量赋值

获得了Field的对象后,可以调用get()和set()方法对某个对象中的属性进行取值和赋值的操作。

Class studentClass = Class.forName("com.scominfo.demo01.entity.Student");
        Field field = studentClass.getDeclaredField("name");
        Student student = (Student) studentClass.newInstance();
        field.setAccessible(true);//解除私有限定
        field.set(student,"ltp");
        System.out.println(field.get(student));

打印信息如下:

ltp

注意在对私有的成员变量进行赋值操作时,要解除私有限定,调用setAccessible()方法,赋值为true,如果不进行设置,会出现IllegalAccessException错误

获取成员方法

这里,新建一个如下的类

public class TestMethod {
    private String name;

    public void isMyname(String name){
        System.out.println("My name is"+name);
    }

    private String returnMyName(){
        return name;
    }
}

和之前一样,使用getDeclaredMethods()可以获得所有方法,使用getMethods()仅仅能获取到公有的方法。

Class methodClass = Class.forName("com.scominfo.demo01.entity.TestMethod");
        Method[] declaredMethods = methodClass.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("所有方法"+declaredMethod);
        }
        Method[] methods = methodClass.getMethods();
        for (Method method : methods) {
            System.out.println("公有方法"+method);
        }

打印结果如下:

所有方法public void com.scominfo.demo01.entity.TestMethod.isMyname(java.lang.String)
所有方法private java.lang.String com.scominfo.demo01.entity.TestMethod.returnMyName()
公有方法public void com.scominfo.demo01.entity.TestMethod.isMyname(java.lang.String)

getDeclaredMethods()方法会获得自身和实现的接口中所有的方法,但是不会获得继承来的方法,getMethods()方法会获得所有的无论是实现的接口还是继承来的public的方法

获取指定的方法

通过方法名和方法中的参数类型就可以获得指定的方法

Class methodClass = Class.forName("com.scominfo.demo01.entity.TestMethod");
        Method returnMyName = methodClass.getDeclaredMethod("returnMyName", null);
        System.out.println(returnMyName);

打印如下:

private java.lang.String com.scominfo.demo01.entity.TestMethod.returnMyName()

使用指定方法

使用Class的invoke方法即可调用具体方法,但是要注意私有方法需要通过setAccessible(true)解除私有限定才能调用。

Class methodClass = Class.forName("com.scominfo.demo01.entity.TestMethod");
        Method isMyname = methodClass.getDeclaredMethod("isMyname", String.class);
        TestMethod testMethod = (TestMethod) methodClass.newInstance();
        isMyname.setAccessible(true);//解决私有限定
        isMyname.invoke(testMethod, "ltp");

具体案例:通过反射将Map类转为对象

实体类依旧使用之前的student类。map转bean有很多工具类都可以使用,但是我们可以自己通过反射来手写一个。

void test005() throws Exception {
        Map<String,Object> map=new HashMap<>();{
            {
                map.put("name","ltp");
                map.put("age",20);
                map.put("sex","男");
                map.put("sss","xxx");
            }
        }
        Student student = mapToBean(map, Student.class);
        System.out.println(student.toString());

    }

    public static <T> T mapToBean(Map<String,Object> map,Class<T> clazz) throws Exception{
        T obj=clazz.newInstance();
        if(map !=null && map.size() > 0){
            for (Map.Entry<String, Object> entry : map.entrySet()) {//循环获取到键值对
                String propertyName = entry.getKey();
                Object value = entry.getValue();//类型不确定
                //这里我们获取到了属性名和属性的值,但是我们并不确定属性值的类型。
                //所以,我们需要先获取到他的变量对象,也就是Filed
                //但是我们需要注意,我们收到的map内,可能还含有其他的字段,而这些字段是不能转为bean的。所以需要排除掉。
                Field field = getClassField(clazz, propertyName);
                if(field == null){
                    continue;
                }
                //按照上面教程,我们是不是可以直接使用field.set就可以给变量赋值了?但是,这里要注意的是,这样可能会产生类型转换错误
                //这是因为在使用反射获取或者修改一个变量的值时,编译器不会进行自动装/拆箱。所以我们需要换个思路,使用get,int方法来复制
                //还有就是变量类型的问题,当然这可以通过判断变量类型来判断是否添加setAccessible来解决。
                //综上所述,使用set方法是最好的选择
                String setMethodName = "set"
                        + propertyName.substring(0, 1).toUpperCase()
                        + propertyName.substring(1);
                //获取方法
                Method setMethod = clazz.getMethod(setMethodName, field.getType());
                //调用此方法赋值,这里又要注意,赋值,应该对应所需要填入的value类型,我们现在的value是Object的,所以需要转换
                value = convertValType(value, field.getType());
                setMethod.invoke(obj,value);
            }
        }
        return obj;
    }

    private static Field getClassField(Class<?> clazz, String fieldName){
        //通过clazz获取到所有变量
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            if(field.getName().equals(fieldName)){
                return field;
            }
        }
        //这里应该再查找一次父类,因为可能这个类集成了某父类
        Class<?> superClass = clazz.getSuperclass();
        //简单的递归一下
        if (superClass != null) {
            return getClassField(superClass, fieldName);
        }
        return null;
    }


    private static Object convertValType(Object value, Class<?> fieldTypeClass) {
        Object retVal = null;
        if (Long.class.getName().equals(fieldTypeClass.getName())
                || long.class.getName().equals(fieldTypeClass.getName())) {
            retVal = Long.parseLong(value.toString());
        } else if (Integer.class.getName().equals(fieldTypeClass.getName())
                || int.class.getName().equals(fieldTypeClass.getName())) {
            retVal = Integer.parseInt(value.toString());
        } else if (Float.class.getName().equals(fieldTypeClass.getName())
                || float.class.getName().equals(fieldTypeClass.getName())) {
            retVal = Float.parseFloat(value.toString());
        } else if (Double.class.getName().equals(fieldTypeClass.getName())
                || double.class.getName().equals(fieldTypeClass.getName())) {
            retVal = Double.parseDouble(value.toString());
        } else {
            retVal = value;
        }
        return retVal;
    }

总结

在使用反射修改某个对象的成员变量前你要明白,这样做会造成一定程度的性能开销,因为在反射时这样的操作需要引发许多额外操作,比如验证访问权限等。只在特殊情况下这么做。此文的目的是理解,而不是让我们在程序中大面积使用它。

posted @ 2020-07-10 16:05  LtPpp  阅读(166)  评论(0)    收藏  举报