使用反射生成并操作对象

时间:2020-03-26
本文章向大家介绍使用反射生成并操作对象,主要包括使用反射生成并操作对象使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Class 对象可以获得该类里的方法(由 Method 对象表示)、构造器(由 Constructor 对象表示)、成员变量(由 Field 对象表示),这三个类都位于 java.lang.reflect 包下,并实现了 java.lang.reflect.Member 接口。程序可以通过对象来执行对应的方法,通过 Constructor 对象来调用对应的构造器创建实例,能通过 Field 对象直接访问并修改对象的成员变量值。

创建对象

通过反射来生成对象需要先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建该 Class 对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。

在很多 Java EE 框架中都需要根据配置文件信息来创建 Java 对象,从配置文件读取的只是某个类的字符串类名,程序需要根据该字符串来创建对应的实例,就必须使用反射。

下面程序就实现了一个简单的对象池,该对象池会根据配置文件读取 key-value 对,然后创建这些对象,并将这些对象放入一个 HashMap 中。

public class ObjectPoolFactory {
    // 定义一个对象池,前面是对象名,后面是实际对象
    private Map<String, Object> objectPool = new HashMap<>();

    // 定义一个创建对象的方法
    // 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
    private Object createObject(String clazzName) throws Exception, IllegalAccessException, ClassNotFoundException {
        // 根据字符串来获取对应的Class对象
        Class<?> clazz = Class.forName(clazzName);
        // 使用clazz对应类的默认构造器创建实例
        return clazz.getConstructor().newInstance();
    }

    // 该方法根据指定文件来初始化对象池
    // 它会根据配置文件来创建对象
    public void initPool(String fileName)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream(fileName)) {
            Properties props = new Properties();
            props.load(fis);
            for (String name : props.stringPropertyNames()) {
                // 每取出一对key-value对,就根据value创建一个对象
                // 调用createObject()创建对象,并将对象添加到对象池中
                objectPool.put(name, createObject(props.getProperty(name)));
            }
        } catch (Exception ex) {
            System.out.println("读取" + fileName + "异常");
        }
    }

    public Object getObject(String name) {
        // 从objectPool中取出指定name对应的对象
        return objectPool.get(name);
    }

    public static void main(String[] args) throws Exception {
        ObjectPoolFactory pf = new ObjectPoolFactory();
        pf.initPool("obj.txt");
        System.out.println(pf.getObject("a")); //
        System.out.println(pf.getObject("b")); //
    }
}

上面程序中 createObject() 方法里的两行粗体字代码就是根据字符串来创建 Java 对象的关键代码,程序调用 Class 对象的 newInstance() 方法即可创建一个 Java 对象。程序中的 initPool() 方法会读取属性文件,对属性文件中每个 key-value 对创建一个 Java 对象,其中 value 是该 Java 对象的实现类,而 key 是该 Java 对象放入对象池中的名字。为该程序提供如下属性配置文件。

a=java.util.Date
b=javax.swing.JFrame

编译、运行上面的 ObjectPoolFactory 程序,执行到 main 方法中的①号代码处,将看到输出系统当前时间——这表明对象池中已经有了一个名为a的对象,该对象是一个 java.util.Date 对象。执行到②号代码处,将看到输出一个 JFrame 对象。

提示:这种使用配置文件来配置对象,然后由程序根据配置文件来创建对象的方式非常有用,大名鼎鼎的 Spring 框架就采用这种方式大大简化了 Java EE 应用的开发。当然,Spring 采用的是 XML 配置文件——毕竟属性文件能配置的信息太有限了,而 XML 配置文件能配置的信息就丰富多。

如果不想利用默认构造器来创建 Java 对象,而想利用指定的构造器来创建 Java 对象,则需要利用 Constructor 对象,每个 Constructor 对应一个构造器。为了利用指定的构造器来创建 Java 对象,需要如下三个步骤。

  1. 获取该类的 Class 对象。
  2. 利用 Class 对象的 getConstructor() 方法来获取指定的构造器。
  3. 调用 Constructor 的 newInstance() 方法来创建 Java 对象。

下面程序利用反射来创建一个 JFrame 对象,而且使用指定的构造器。

public class CreateJFrame {
    public static void main(String[] args) throws Exception {
        // 获取JFrame对应的Class对象
        Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
        // 获取JFrame中带一个字符串参数的构造器
        Constructor ctor = jframeClazz.getConstructor(String.class);
        // 调用Constructor的newInstance方法创建对象
        Object obj = ctor.newInstance("测试窗口");
        // 输出JFrame对象
        System.out.println(obj);
    }
}

上面程序中第一行粗休字代码用于获取 JFrame 类的指定构造器,前面已经提到:如果要唯一地确定某类中的构造器,只要指定构造器的形参列表即可。第一行粗体字代码获取构造器时传入了一个 String 类型,即表明想获取只有一个字符串参数的构造器。

程序中第二行粗体字代码使用指定构造器的 newInstance() 方法来创建一个 Java 对象,当调用 Constructor 对象的 newInstance() 方法时通常需要传入参数,因为调用 Constructor 的 newInstance() 方法实际上等于调用它对应的构造器,传给 newInstance() 方法的参数将作为对应构造器的参数。

对于上面的 CreateFrame.java 中已知 java.swing.JFrame 类的情形,通常没有必要使用反射来创建该对象,毕竟通过反射创建对象时性能要稍低一些。实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。

调用方法

当获得某个类对应的 Class 对象后,就可以通过该 Class 对象的 getMethods() 方法或者 getMethod()方法来获取全部方法或指定方法——这两个方法的返回值是 Method 数组,或者 Method 对象。

每个 Method 对象对应一个方法,获得 Method 对象后,程序就可通过该 Method 来调用它对应的方法。在 Method 里包含一个 Invoke() 方法,该方法的签名如下。

  • Object invoke(Object obj, Object...args):该方法中的 obj 是执行该方法的主调,后面的 args 是执行该方法时传入该方法的实参。

下面程序对前面的对象池工厂进行加强,允许在配置文件中增加配置对象的成员变量的值,对象池工厂会读取为该对象配置的成员变量值,并利用该对象对应的 setter 方法设置成员变量的值。

public class ExtendedObjectPoolFactory {
    // 定义一个对象池,前面是对象名,后面是实际对象
    private Map<String, Object> objectPool = new HashMap<>();
    private Properties config = new Properties();

    // 从指定属性文件中初始化Properties对象
    public void init(String fileName) {
        try (FileInputStream fis = new FileInputStream(fileName)) {
            config.load(fis);
        } catch (IOException ex) {
            System.out.println("读取" + fileName + "异常");
        }
    }

    // 定义一个创建对象的方法
    // 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
    private Object createObject(String clazzName) throws Exception {
        // 根据字符串来获取对应的Class对象
        Class<?> clazz = Class.forName(clazzName);
        // 使用clazz对应类的默认构造器创建实例
        return clazz.getConstructor().newInstance();
    }

    // 该方法根据指定文件来初始化对象池
    // 它会根据配置文件来创建对象
    public void initPool() throws Exception {
        for (String name : config.stringPropertyNames()) {
            // 每取出一个key-value对,如果key中不包含百分号(%)
            // 这就表明是根据value来创建一个对象
            // 调用createObject创建对象,并将对象添加到对象池中
            if (!name.contains("%")) {
                objectPool.put(name, createObject(config.getProperty(name)));
            }
        }
    }

    // 该方法将会根据属性文件来调用指定对象的setter方法
    public void initProperty() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        for (String name : config.stringPropertyNames()) {
            // 每取出一对key-value对,如果key中包含百分号(%)
            // 即可认为该key用于控制调用对象的setter方法设置值
            // %前半为对象名字,后半控制setter方法名
            if (name.contains("%")) {
                // 将配置文件中的key按%分割
                String[] objAndProp = name.split("%");
                // 取出调用setter方法的参数值
                Object target = getObject(objAndProp[0]);
                // 获取setter方法名:set + "首字母大写" + 剩下部分
                String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase() + objAndProp[1].substring(1);
                // 通过target的getClass()获取它的实现类所对应的Class对象
                Class<?> targetClass = target.getClass();
                // 获取希望调用的setter方法
                Method mtd = targetClass.getMethod(mtdName, String.class);
                // 通过Method的invoke方法执行setter方法
                // 将config.getProperty(name)的值作为调用setter方法的参数
                mtd.invoke(target, config.getProperty(name));
            }
        }
    }

    public Object getObject(String name) {
        // 从objectPool中取出指定name对应的对象
        return objectPool.get(name);
    }

    public static void main(String[] args) throws Exception {
        ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
        epf.init("extObj.txt");
        epf.initPool();
        epf.initProperty();
        System.out.println(epf.getObject("a"));
    }
}

上面程序中 initProperty() 方法里的第一行粗体字代码获取目标类中包含一个 String 参数的 setter 方法,第二行粗体字代码通过调用 Method 的 invoke() 方法来执行该 setter 方法,该方法执行完成后,就相当于执行了目标对象的 setter 方法。为上面程序提供如下配置文件。

a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title

上面配置文件中的 a%title 行表明希望调用a对象的 setTitle() 方法,调用该方法的参数值为 Test Title。编译、运行上面的 ExtendedObjectPoolFactory.java 程序,可以看到输出一个 JFrame 窗口,该窗口的标题为 Test Title。

提示:Spring 框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行管理的,从而实现了较好的解耦。这也是 Spring 框架的 IOC 的秘密。

当通过 Method 的 invoke() 方法来调用对应的方法时,Java 会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的 private 方法,则可以先调用 Method 对象的如下方法。

  • setAccessible(boolean flag):将 Method 对象的 accessible 设置为指定的布尔值。值为true,指示该 Method 在使用时应该取消 Java 语言的访问权限检查:值为false,则指示该 Method 在使用时要实施 Java 语言的访问权限检查。

注意:实际上,setAccessible() 方法并不属于 Method,而是属于它的父类 AccessibleObject。因此 Method、Constructor、Field 都可调用该方法,从而实现通过反射来调用 private 方法、private 构造器和成员变量,下一节将会让读者看到这种示例。也就是说,它们可以通过调用该方法来取消访问权限检查,通过反射即可访问 private 成员。

访问成员变量值

通过 Class 对象的 getFields() 或 getField() 方法可以获取该类所包括的全部成员变量或指定成员变量。Field 提供了如下两组方法来读取或设置成员变量值。

  • getXxx(Object obj):获取 obj 对象的该成员变量的值。此处的 Xxx 对应8种基本类型,如果该成员变量的类型是引用类型,则取消 get 后面的Xxx。
  • setXxx(Object obj, Xxx val):将 obj 对象的该成员变量设置成值。此处的 Xxx 对应8种基本类型,如果该成员变量的类型是引用类型,则取消 set 后面的Xxx。

使用这两个方法可以随意地访问指定对象的所有成员变量,包括 private 修饰的成员变量。

class Person {
    private String name;
    private int age;

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

public class FieldTest {
    public static void main(String[] args) throws Exception {
        // 创建一个Person对象
        Person p = new Person();
        // 获取Person类对应的Class对象
        Class<Person> personClazz = Person.class;
        // 获取Person的名为name的成员变量
        // 使用getDeclaredField()方法表明可获取各种访问控制符的成员变量
        Field nameField = personClazz.getDeclaredField("name");
        // 设置通过反射访问该成员变量时取消访问权限检查
        nameField.setAccessible(true);
        // 调用set()方法为p对象的name成员变量设置值
        nameField.set(p, "Yeeku.H.Lee");
        // 获取Person类名为age的成员变量
        Field ageField = personClazz.getDeclaredField("age");
        // 设置通过反射访问该成员变量时取消访问权限检查
        ageField.setAccessible(true);
        // 调用setInt()方法为p对象的age成员变量设置值
        ageField.setInt(p, 30);
        System.out.println(p);
    }
}

上面程序中先定义了一个 Person 类,该类里包含两个 private 成员变量:name 和 age,在通常情况下,这两个成员变量只能在 Person 类里访问。但本程序 FieldTest 的 main() 方法中6行粗体字代码通过反射修改了 Person 对象的 name、age 两个成员变量的值。

第一行粗体字代码使用 getDeclaredField() 方法获取了名为 name 的成员变量,注意此处不是使用 getField()方法,因为 getField() 方法只能获取 public 访问控制的成员变量,而 getDeclaredField() 方法则可以获取所有的成员变量;第二行粗体字代码则通过反射访问该成员变量时不受访问权限的控制;第三行粗体字代码修改了 Person 对象的 name 成员变量的值。修改 Person 对象的 age 成员变量的值的方式与此完全相同。

编译、运行上面程序,会看到如下输出:

Person[name:Yeeku.H.Lee , age:30 ]

操作数组

在 java.lang.reflect 包下还提供了一个 Array 类,Array 对象可以代表所有的数组。程序可以通过使用 Array 来动态地创建数组,操作数组元素等。

Array 提供了如下几类方法。

  • static Object newInstance(Class<?> componentType,int...length):创建一个具有指定的元素类型、指定维度的新数组。
  • static xxx getXxx(Object array, int index):返回 array 数组中第 index 个元素。其中是各种基本数据类型,如果数组元素是引用类型,则该方法变为 get(Object array, int index)。
  • static void setXxx(Object array, int index, xxx val):将 array 数组中第 index 个元素的值设为 val。其中 xxx 是各种基本数据类型,如果数组元素是引用类型,则该方法变成 set(Object array, int index, Object val)。

下面程序示范了如何使用 Array 来生成数组,为指定数组元素赋值,并获取指定数组元素的方式。

public class ArrayTest1 {
    public static void main(String args[]) {
        try {
            // 创建一个元素类型为String ,长度为10的数组
            Object arr = Array.newInstance(String.class, 10);
            // 依次为arr数组中index为5、6的元素赋值
            Array.set(arr, 5, "疯狂Java讲义");
            Array.set(arr, 6, "轻量级Java EE企业应用实战");
            // 依次取出arr数组中index为5、6的元素的值
            Object book1 = Array.get(arr, 5);
            Object book2 = Array.get(arr, 6);
            // 输出arr数组中index为5、6的元素
            System.out.println(book1);
            System.out.println(book2);
        } catch (Throwable e) {
            System.err.println(e);
        }
    }
}

上面程序中三行粗体字代码分别是通过 Array 创建数组,为数组元素设置值,访问数组元素的值的示例代码,程序通过使用 Array 就可以动态地创建并操作数组。

下面程序比上面程序稍微复杂一点,下面程序使用 Array 类创建了一个三维数组。

public class ArrayTest2 {
    public static void main(String args[]) {
        /*
         * 创建一个三维数组。 根据前面介绍数组时讲的:三维数组也是一维数组, 是数组元素是二维数组的一维数组,
         * 因此可以认为arr是长度为3的一维数组
         */
        Object arr = Array.newInstance(String.class, 3, 4, 10);
        // 获取arr数组中index为2的元素,该元素应该是二维数组
        Object arrObj = Array.get(arr, 2);
        // 使用Array为二维数组的数组元素赋值。二维数组的数组元素是一维数组,
        // 所以传入Array的set()方法的第三个参数是一维数组。
        Array.set(arrObj, 2, new String[] { "疯狂Java讲义", "轻量级Java EE企业应用实战" });
        // 获取arrObj数组中index为3的元素,该元素应该是一维数组。
        Object anArr = Array.get(arrObj, 3);
        Array.set(anArr, 8, "疯狂Android讲义");
        // 将arr强制类型转换为三维数组
        String[][][] cast = (String[][][]) arr;
        // 获取cast三维数组中指定元素的值
        System.out.println(cast[2][3][8]);
        System.out.println(cast[2][2][0]);
        System.out.println(cast[2][2][1]);
    }
}

上面程序的第一行粗体字代码使用 Array 创建了一个三维数组,程序中较难理解的地方是第二段粗体字代码部分,使用 Array 为 arrObj 的指定元素赋值,相当于为二维数组的元素赋值。由于二维数组的元素是一维数组,所以程序传入的参数是一个一维数组对象。

运行上面程序,将看到 cast[2][3][8]、cast[2][2][0]、cast[2][2][1] 元素都有值,这些值就是刚才程序通过反射传入的数组元素值。

原文地址:https://www.cnblogs.com/jwen1994/p/12574465.html