反射

我们在编程的过程中,为了更好地描述现实世界的事物,会将现实中的事物看做一个一个的对象,那在程序设计中是用什么来描述对象会有哪些属性或者方法呢?那就是用类了,其实在现实社会中也一样,一个一个的人是属于人类这一个类别的,那在程序设计中,有没有什么东西是用来描述类的呢?有的,Java中就将每个类看做是Class这个类的一个对象,也就是字节码对象,通过字节码对象,我们就能够知道这个类拥有哪些成员了。

1.字节码对象的获取

字节码对象的获取方式一共有三种,第一种是直接通过类名,每个类都会有一个静态成员属性 class ,直接通过这个属性就可以得到这个类的字节码对象;第二种就是要通过一个类的对象,每个对象都会有一个 getClass() 方法,这个方法是继承 Object 这个类得到的,通过对象调用这个方法就可以得到该对象所属类的字节码对象;第三种方法是通过 Class 这个类的静态方法 forName(String className) 来得到字节码对象,传入的字符串参数是这个类的全类名,就是包含完整包名的类名。这三种方式第三种只需要字符串参数就可以了,因此使用的最多。

现在假设有一个 Student 类,那获取这个类的字节码文件就有以下三种方式:

a:Class clazz = Student.class;
b:Student s = new Student();    Class clazz = s.getClass();
c:Class clazz = Class.forName("party.laucloud.Student");

使用一个类的字节码对象主要是为了获取这个类的各个组成部分,并使用它们,而一个类的组成部分主要就是构造器,成员变量和成员方法,下面就依次演示如何得到这各个部分。

2.获取构造器

为了演示获取构造器和下面所有的代码,先在这里把要操作的类放上来

package day13;

public class Student {
    private String name;
    int age;

    public Student(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public Student() {
        super();
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

那下面就直接演示获取构造器了

public class ClassDemo2 {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException,
            InvocationTargetException {
        Class clazz = Class.forName("day13.Student");

        Constructor[] cs = clazz.getConstructors();
        for (int i = 0; i < cs.length; i++) {
            System.out.println(cs[i]);
        }

        Constructor c = clazz.getConstructor();
        Object s = c.newInstance();
        System.out.println(s);

        Constructor c2 = clazz.getConstructor(String.class, int.class);
        Object s2 = c2.newInstance("小樱", 17);
        System.out.println(s2);
    }
}

上面的程序演示了获取 Student 的空参构造和有参构造,并对它们进行了调用

结果如下:

public day13.Student(java.lang.String,int)
public day13.Student()
Student [name=null, age=0]
Student [name=小樱, age=17]

3.获取成员变量

先放代码:

public class FieldDemo {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchFieldException, SecurityException, IllegalArgumentException,
            IllegalAccessException, InstantiationException {
        Class<?> clazz = Class.forName("day13.Student");
        Object stu = clazz.newInstance();
        Field[] fs = clazz.getDeclaredFields();
        for (int i = 0; i < fs.length; i++) {
            System.out.println(fs[i]);
        }
        Field fAge = clazz.getDeclaredField("age");
        Object age = fAge.get(stu);
        System.out.println(age);

        fAge.set(stu, 18);
        System.out.println(stu);
    }
}

这次字节码对象获取成员属性的时候是使用的 getDeclaredFields() 方法,而上面获取构造器是直接使用的 getConstructors() 方法,其实获取成员变量也可以使用 getFields() 方法,也是用来获取类的成员变量的,其实还不止获取成员变量如此,获取类的其它组成部分都会有这样的差别,就是一个方法中间带有 Declared ,一个没有,那这两者的区别是什么呢?就以获取成员变量为例来说明, getFields() 是获取本类以及父类还有超类中所有以 public 修饰的成员变量,而 getDeclaredFields() 而是获取本类中声明的所有成员变量,这就是两者的差别。

其实获取成员变量还有一个方法,那就是获取指定的成员变量,通过 getDeclaredField(String name) 来实现,当然这和上面也是一样,public 修饰的成员变量可以通过 getField(String name) 获得,而其它的就只能通过 getDeclaredField(String name) 来访问了。

4.获取成员方法

先把代码放上来:

public class MethodDemo {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("day13.Student");

        Object stu = clazz.newInstance();

        Method method = clazz.getMethod("getName");
        System.out.println(method.invoke(stu));


        Method m2 = clazz.getDeclaredMethod("setName", String.class);
        m2.setAccessible(true);
        m2.invoke(stu, "李四");

        System.out.println(stu);
    }
}

这里通过反射调用 setName() 方法的时候,其中有一句代码是 m2.setAccessible(true);,如果不加这一句的话程序会报错,报错提示是说不能访问一个以 private 修饰的成员方法,这就是Java中的访问检查机制,因为我们这个程序是在 Student 类的外面,因此是不能访问 Student类中的私有成员方法的,这和我们之前编程的逻辑也是一样的,那如果想在 Student 类的外面访问这个私有方法怎么办呢?这里就要看API文档了,成员方法所属的类是 java.lang.reflect.Method,而它的直接父类就是 java.lang.reflect.AccessibleObject,没错,这里面就有取消Java中访问检查的方法,那就是 setAccessible(boolean flag),这是一个普通的成员方法,由于 Method 是这个类的子类,所以也就继承了这一个方法,因此要想访问 Student 类中的私有成员方法,就应该使用对应的 Method 对象调用上面这个方法,那也就是 m2.setAccessible(true); 了,其实像这样暴力访问的方式并不好,因为破坏了 Studnet 类的封装性。

其实这里主要是为了演示这种现象才把 setName() 方法设置为私有的,像上面的 getter()setter() 方法主要就是为了让外部程序访问类中的成员变量的,因此是不应该设置为私有的。

5.BeanUtils的使用

BeanUtilsApache 这个组织发布的操作 JavaBean 的工具组件,里面封装了很多实用方法,可以提高我们的开发效率,说起来 Apache 这个组织真的挺强大的,发布了好多关于 Java 的组件和应用,比如 Tomcat 服务器,DBUtils 操作数据库的组件。

我使用的 BeanUtils 版本号是 commons-beanutils-1.8.3.jar,而使用它的话还需要另一个组件 commons-logging-1.1.1.jar,这两个都可以在网上搜索然后在官网进行下载的,下载工具的话我还是比较喜欢到官网,就算下载 JDK 或者 MySQL 需要先注册一下,还是感觉东西把握在自己手中的感觉,其实如果第一次下载某种东西感觉很陌生的话,多尝试一下就好了,然后多下载两遍就熟悉了,就会发觉获取工具其实蛮简单的。

5.1 导入jar包

使用上面所说的工具,必须导入相应的jar包,直接在项目中新建一个文件夹,比如 lib,然后将上面两个 jar 包拷贝到里面,打开 lib 文件夹,选中刚拷贝进来的两个 jar 包,点击右键,选择 Build Path ,再选择 Add to Build Path,这样 jar 就导入成功了。

5.2 使用BeanUtils

这里主要介绍 BeanUtils 类中的三个方法:

1.public static String getProperty(Object bean, String name)
2.public static void setProperty(Object bean, String name, Object value)
3.public static void populate(Object bean, Map properties)

很明显可以看出,第一个方法是获取 bean 对象中某个属性的值,第二个就是为 bean 对象中的某个属性赋值,第三个方法就是使用 Map 集合中的对应的键值对为 bean 对象赋值。

首先自己先定义一个人物类:

public class Person {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    public Person() {
        super();
    }

    public Person(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

下面再演示 BeanUtils 类的相关方法:

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;

public class BeanUtilsDemo {
    public static void main(String[] args) throws ReflectiveOperationException {
        // mathod();

        Person p2 = new Person();

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("name", "小雪");
        map.put("age", 17);
        map.put("number", 14526);

        BeanUtils.populate(p2, map);
        System.out.println(p2);
    }

    public static void mathod() throws IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        Person p = new Person();

        System.out.println(p);

        BeanUtils.setProperty(p, "name", "青");
        BeanUtils.setProperty(p, "age", 18);

        System.out.println(p);

        String name = BeanUtils.getProperty(p, "name");
        String age = BeanUtils.getProperty(p, "age");
        System.out.println("name:" + name);
        System.out.println("age:" + age);
    }
}

使用 BeanUtils 确实可以提高开发效率,使用起来也比较简单。

6.自己创建BeanUtils相似工具类

其实 BeanUtils 中获取对象属性值和为属性赋值也还是走的 gettersetter 方法,可以做个试验,就是将 Person 类中的某些 gettersetter 方法设置为 private,程序就会出现异常,并且获取到的值就会是 null 或者 0,这就说明 BeanUtils 绝对走的不是上面我们说过的暴力访问的方法,那样也确实不好,破坏了类的封装性,那就完全背离了 JavaBean 的设计初衷了,知道这些。我们也就可以自己尝试着写一下类似 BeanUtils 的程序了。

直接放上代码,工具类:

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;

public class MyBeanUtils {
    public static void setProperty(Object bean, String name, Object value)
            throws ReflectiveOperationException {
        Class clazz = bean.getClass();
        String methodName = "set" + name.substring(0, 1).toUpperCase()
                + name.substring(1);
        try {
            Method method = clazz.getDeclaredMethod(methodName,
                    value.getClass());
            method.invoke(bean, value);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public static String getProperty(Object bean, String name)
            throws ReflectiveOperationException {
        Class clazz = bean.getClass();
        String methodName = "get" + name.substring(0, 1).toUpperCase()
                + name.substring(1);
        Object value = null;
        try {
            Method method = clazz.getDeclaredMethod(methodName);
            value = method.invoke(bean);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return "" + value;
    }

    public static void populate(Object bean, Map map) throws ReflectiveOperationException {
        Set<String> keySet = map.keySet();
        Class clazz = bean.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            String name =  fields[i].getName();
            if(keySet.contains(name)){
                Object value = map.get(name);
                setProperty(bean,name,value);
            }
        }
    }
}

测试类:

import java.util.HashMap;
import java.util.Map;

public class MyBeanUtilsDemo {
    public static void main(String[] args) throws ReflectiveOperationException {
        Person p2 = new Person();

        System.out.println(p2);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("name", "小雪");
        map.put("age", 17);
        map.put("number", 14526);

        MyBeanUtils.populate(p2,map);
        System.out.println(p2);
    }

    public static Person method() throws ReflectiveOperationException {
        Person p = new Person();
        System.out.println(p);

        MyBeanUtils.setProperty(p, "name", "Lau Cloud");
        MyBeanUtils.setProperty(p, "age", 17);

        System.out.println(p);

        String name = MyBeanUtils.getProperty(p, "name");
        String age = MyBeanUtils.getProperty(p, "age");
        System.out.println("name:" + name);
        System.out.println("age:" + age);
        return p;
    }
}

把这个小工具类写出来还是有点小激动的,毕竟自己用所学的知识实现了自己所想的东西,而且还不是那么简单粗暴的方法,这说明自己还是费了心思的。

7.总结

反射在一般的应用开发中用的比较少,但是编写一些工具还是可以的,就我现在的理解而言,字节码对象就是用来描述一个类的,而类的组成大概就是构造器、成员变量和成员方法,因此最重要的就是使用反射获取这些组成部分,其它的就是要注意一些细节吧,把握大的,留意小的,就差不多了吧。

坚持原创技术分享,您的支持将鼓励我继续创作!