Java反射

能够分析类的能力的程序称为反射。Java的反射是一种功能强大并且复杂的机制,一般使用它都是用来构建一些工具或者框架等,比如spring框架的IOC技术,fastjson将json字符串转换成POJO的能力,都是使用了反射。

1、反射的使用场景

(1)在程序运行中分析类的能力,比如获取该类的注解、构造器、属性和方法等信息。

(2)查看或改变某个类的实例对象的状态,比如改变改对象某个属性的值。

(3)调用某个类或者对象的方法。

(4)代理、注解等技术也需要反射的参与。

2、Class类

如果你有学习过JVM,应该会知道在类加载的时候,JVM会把一个类的相关信息加载到方法区中,这些信息用来跟踪每个对象所属的类。保存这些信息的类被称为Class,每个类在同一个类加载器下,一个类只会有一个Class实例。

Class类是分析一个Java类的基础,通过它我们可以获取这个类的类名、构造器、属性和方法等。Java是个面向对象语言,所以获取到的构造器、属性和方法都有对应的Java类型:Field、Method、Constructor分别用于描述类的域(属性)、方法和构造器。

从Java SE5.0开始,Class类已经参数化(即加入了泛型)。要获取一个类的Class实例,一般有一下几种常用方法:

// 通过类的实例去获取
Date date = new Date();
Class<?> clazz1 = date.getClass();
// 通过ClassName.class的方式获取
Class<?> clazz2 = Date.class;
// 通过Class类的静态方法获取,类名必须是完全限定类名,即类名加上包名
// 该方法会抛出一个ClassNotFoundException
Class<?> clazz3 = Class.forName("java.util.Date");

前面已经说过,在同一个类加载器下,一个类只会有一个Class实例,可以通过下面的例子来验证一下,结果是会在控制台打印true:

if ((clazz1 == clazz2) && (clazz1 == clazz3)) {
    System.out.println(true);
}

细心和好奇的读者看到这里可能就会想,上面介绍的都是获取一个类的Class实例,要是需要获取基本类型(int、long等)或者数组的Class实例呢?

Class<?> intClazz = int.class;
Class<?> arrayClazz = int[].class;
Class<?> arrayClass = Integer[].class;

看到这样的写法是不是有点惊讶?(反正我第一次看到的时候是比较吃鲸,大概是孤陋寡闻了点)。

在获取到了一个类的Class实例之后,我们就可以不用通过new的方式去创建一个类的实例了。有一个很有用的newInstance()方法,可以用来创建一个类的实例。newInstance方法调用类默认的构造器(即没有参数的构造器,如果该类没有任何构造方法,编译器或自动帮它生成一个)来新建一个对象。如果这个类没有默认的构造方法,就会抛出异常。(如果需要使用带有参数的构造器去新建一个对象,就必须使用Constructor类中的newInstance方法了,下面会介绍)

大家看到这里或许会觉得花费了那么功夫,才获取到一个类的实例,还不如直接new方便。但是将forName()和newInstance()一起使用,再结合工厂模式,大概就是下面这个样子。从而简化了工厂方法的一大堆if-else写法:

public class FactoryPattern {

    public static Object newInstance(String className) {
        Object instance = null;
        try {
            Class<?> clazz = Class.forName(className);
            instance = clazz.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return instance;
    }

}

3、分析类的能力

利用反射可以检查类的结构,在java.lang.reflect包中有Field、Method、Constructor三个类分别用于描述类的域(属性)、方法和构造器。

3.1 Constructor

Constructor类是用于描述一个类的构造器方法,已经参数化,加入了泛型。用法比较简单,主要有以下几种:

Class<?> clazz = Date.class;
// 获取所有公有的构造器
Constructor<?>[] c2 = clazz.getConstructors();
// 获取所有构造器,包括private和protected的构造器
Constructor<?>[] c1 = clazz.getDeclaredConstructors();
// 获取指定参数类型的构造器,只能是public
Constructor<?> c3 = clazz.getConstructor(long.class);
// 获取指定参数类型的构造器,包括private和protected的构造器
Constructor<?> c4 = clazz.getDeclaredConstructor(long.class);
// 使用带参数的构造器实例化对象
Date date = (Date) c3.newInstance(1L);

3.2 Field

Field类是用于描述一个类的实例域。通过使用Field,我们可以不用同过类的getter和setter就能获取和设置该实例域的值,即使这个实例域使用private修饰(看到这里你也许会想,这样不就破坏了Java的封装?)。不过前提是先调用Field对象的setAccessible()方法,否则会抛出IllegalAccessException。下面给出一个简单示例:

学生类:

public class Student {

    /** 注意是用public修饰的 */
    public String name;
    private int age;
    private String phone;

    public Student() {
    }

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

    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 getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

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

}

测试类:

public class Tests {

    public static void main(String[] args) throws Exception {
        testField();
    }

    public static void testField() throws Exception {
        Student s = new Student("古天乐", 40, "13399988877");
        Class<?> clazz = s.getClass();
        // 获取这个类和其超类的公有域
        Field[] fields1 = clazz.getFields();
        // 获取这个类的所有域,包括本身的私有域,不包括超类的任何域
        Field[] fields2 = clazz.getDeclaredFields();

        Field nameField = clazz.getField("name");
        Field ageField = clazz.getDeclaredField("age");

        // 获取属性的Java类型
        Class<?> javaType = nameField.getType();
        System.out.println(String.format("Java类型:%s", javaType.getName()));

        /* 因为没有调用setAccessible方法,如果直接调用get或者set方法的时候会抛出异常
         * 如果是nameField.get(s)则不会抛出异常,因为它是public修饰的 */
        try {
            ageField.get(s);
        } catch (IllegalAccessException e) {
            System.out.println(String.format("出现异常:%s", e.getMessage()));
        }

        // 设置属性的可访问性
        nameField.setAccessible(true);
        ageField.setAccessible(true);

        /* 获取实例域的值 */
        String nameValue = (String) nameField.get(s);
        int ageValue = ageField.getInt(s);
        System.out.println(String
                .format("通过反射获取实例域的值,name = %s, age = %s", 
                        nameValue, ageValue));

        /* 可以通过AccessibleObject.setAccessible方法一次性设置Field数组的可访问性
         * AccessibleObject类是Field、Method和Constructor的公共超类 */
        AccessibleObject.setAccessible(fields2, true);

        /* 设置实例域的值 */
        // 旧的值: 13399988877
        String oldPhone = s.getPhone();
        Field phoneField = clazz.getDeclaredField("phone");
        // 设置新的值
        phoneField.setAccessible(true);
        phoneField.set(s, "13344455566");
        System.out.println(String
                .format("通过反射设置实例域的值,oldPhone = %s, newPhone = %s", 
                        oldPhone, s.getPhone()));
    }

}

从上面的例子可以注意到一点,由于age是一个int基本类型的属性,这里是通过getInt()方法是获取的。反射机制会自动将这个域打包到相应的对象包装器中。

如果想获取一个类和这个类的超类的所有属性,可以使用下面的方法(包括私有域):

private List<Field> listAllField(Class<?> clasz) {
    List<Field> fieldList = new ArrayList<>();
    do {
        Field[] fields = clasz.getDeclaredFields();
        if (fields != null && fields.length > 0) {
            fieldList.addAll(Arrays.asList(fields));
        }
        // 获取该类的超类
        clasz = clasz.getSuperclass();
    } while (clasz != null);
    return fieldList;
}

3.3 Method

Method类是用于描述一个类的方法。通过Method类,我们可以做到调用任意方法。反射机制的Method经常跟代理技术一起使用,用来给类添加新的功能。比如spring的AOP就是使用了代理。下面看看常用接口:

public class Tests {

    public static void main(String[] args) throws Exception {
        testMethod();
    }

    public static void testMethod() throws Exception {
        Student s = new Student("古天乐", 40, "13399988877");
        Class<?> clazz = Student.class;

        Method[] methods1 = clazz.getMethods();
        Method[] methods2 = clazz.getDeclaredMethods();

        /* 方法签名:getMethod(String name, Class<?>... parameterTypes)
         * 获取所有的公有方法,包括继承过来的,如果方法没有参数,可以省略第二个参数 */
        Method getName = clazz.getMethod("getName");
        /* 方法签名:getDeclaredMethod(String name, Class<?>... parameterTypes)
         * 获取该类的所有方法,包括私有的,但是不包括继承过来的
         * 如果方法没有参数,可以省略第二个参数 */
        Method setPhone = clazz.getDeclaredMethod("setPhone", String.class);
        
        /* Method类的invoke用来表示调用某个对象的方法
         * 签名为:invoke(Object obj, Object... args)
         * 第一个参数是实际的对象,第二个参数使用了可变参数类型
         * 是指调用该方法需要传递的参数 */

        // 调用getter获取函数返回值
        String nameValue = (String) getName.invoke(s);
        System.out.println(String
                .format("通过getter获取的值:name = %s", nameValue));

        String oldPhone = s.getPhone();
        // 调用setter设置新的值
        setPhone.invoke(s, "13344455566");
        System.out.println(String
                .format("通过setter设置的值:phone = %s", s.getPhone()));
        
        // 获取方法返回的java类型
        Class<?> returnType = getName.getReturnType();
        System.out.println(String
                .format("getName方法返回的java类型:%s", returnType.getName()));
    }

}

4、综合运用

通过上面的介绍,不知道大家是否已经对java的反射有了一点的了解。Java的反射是一种很强大的机制(也许目前还没有看出来反射到底能用来干什么),使用反射可以实现一些比较强大的功能。下面我就给出一个比较实用的例子(之前实习的时候写的),实现的功能就是把一张Excel表格的数据转换成为一个Java Bean。这里的核心代码比较简短,但是逻辑有点复杂,同时还使用了几个辅助的方法。要求大家对Java泛型还有JXL(用来操作excel)有些了解。

测试的Excel(2003版的格式,即.xls格式)如图:

控制台打印:

Student [name=古天乐, age=40, phone=13344455566]
Student [name=刘德华, age=50, phone=13399988877]

代码如下:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jxl.Cell;
import jxl.Sheet;
import jxl.Workbook;

public class Excel {

    public static void main(String[] args) throws Exception {
        byte[] data = readFile("E:/student.xls");
        String title = "姓名|年龄|手机";
        String keys = "name|age|phone";
        List<Student> students = importExcel(title.split("[|]"), 
                keys.split("[|]"), data, Student.class);
        for (Student s : students) {
            System.out.println(s);
        }
    }

    /**
     * @param titles 导出的数据标题 必须和keys同时存在,且顺序保持一致
     * @param keys   导出数据的keys,必须和title同时存,且顺序保持一致
     * @param data   excel文件的byte[]类型
     * @param type   对应的bean类型
     * @return
     * @throws Exception
     */
    public static <T> List<T> importExcel(String[] titles, String[] keys, 
            byte[] data, Class<T> type) throws Exception {
        List<T> ts = new ArrayList<>();
        Integer columns = 0;
        // 用于保存由excel表头到bean的属性的映射关系
        Map<String, Integer> titleToKey = new HashMap<>(16);
        for (int i = 0; i < titles.length; i++) {
            titleToKey.put(titles[i], i);
        }
        try (InputStream is = new ByteArrayInputStream(data)) {
            Workbook workbook = Workbook.getWorkbook(is);
            Sheet sheet = workbook.getSheet(0);
            List<Method> setters = new ArrayList<>(16);
            // 遍历表头
            for (int i = 0; i < sheet.getColumns(); i++) {
                Cell cell = sheet.getCell(i, 0);
                String title = cell.getContents().trim();
                if (title == null || "".equals(title)) {
                    continue;
                }
                columns++;
                String key = keys[titleToKey.get(title)];
                // 获取bean的getter方法
                Method getter = type.getMethod("get" + firstLetterUpper(key));
                Class<?> returnType = getter.getReturnType();
                // 获取bean的setter方法
                Method setter = type.getMethod("set" + firstLetterUpper(key), 
                        returnType);
                setters.add(setter);
            }
            // 遍历表数据
            for (int i = 1; i < sheet.getRows(); i++) {
                T t = type.newInstance();
                for (int j = 0; j < columns; j++) {
                    Cell cell = sheet.getCell(j, i);
                    String value = cell.getContents();
                    if (value != null && !"".equals(value)) {
                        Method setter = setters.get(j);
                        Class<?> paramType = setter.getParameterTypes()[0];
                        Object param = stringToObject(paramType, value);
                        // 调用setter方法
                        setter.invoke(t, param);
                    }
                }
                ts.add(t);
            }
            workbook.close();
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
        return ts;
    }

    /**
     * 将指定路径的文件转换成byte[]
     * @param filePath
     * @return
     * @throws IOException 
     */
    public static byte[] readFile(String filePath) {
        byte[] data = null;
        File excel = new File(filePath);
        try (FileInputStream fis = new FileInputStream(excel);
                ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
            byte[] buffer = new byte[1024 * 4];
            int n = 0;
            while ((n = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, n);
            }
            data = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return data;
    }

    /**
     * 首字母大写
     * @param string
     * @return
     */
    public static String firstLetterUpper(String string) {
        if (string == null) {
            return "";
        }
        char[] c = string.toCharArray();
        if (c[0] >= 'a' && c[0] <= 'z') {
            c[0] = (char) (c[0] - 32);
        }
        return new String(c);
    }

    /**
     * 将字符串的类型转换为给定的java类型
     * @param type  希望生成的java类型,目前只支持
     *              基本类型的包装器类型、String和java.util.Date类型
     * @param value 字符串值
     * @return
     */
    private static Object stringToObject(Class<?> type, String value) {
        Object param = null;
        String javaType = type.getName();
        switch (javaType) {
            case "java.lang.String":
                param = value;
                break;
            case "byte":
            case "java.lang.Byte":
                param = Byte.valueOf(value);
                break;
            case "boolean":
            case "java.lang.Boolean":
                param = Boolean.valueOf(value);
                break;
            case "short":
            case "java.lang.Short":
                param = Short.valueOf(value);
                break;
            case "int":
            case "java.lang.Integer":
                param = Integer.valueOf(value);
                break;
            case "long":
            case "java.lang.Long":
                param = Long.valueOf(value);
                break;
            case "float":
            case "java.lang.Float":
                param = Float.valueOf(value);
                break;
            case "double":
            case "java.lang.Double":
                param = Double.valueOf(value);
                break;
            case "java.util.Date":
                break;
            default:
                break;
        }
        return param;
    }

}

5、总结

终于写到了总结这一步,第一次写博客,远远比自己想象中要花的时间长,也比较费精神。如果有什么写得不好的地方或者有错误的地方请大家多多包含和指出。

这里简单介绍了一下Java反射常见用法,虽然反射的功能很强大,但是使用反射java的编译器就无法对你的代码进行优化,加上Method类的invoke方法也比直接通过对象调用的方式慢。所以反射还是慎用。最后给出一个例子供学习参考。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值