Java反射机制详解


本文摘自微信公众号-小旋锋:《Java反射机制详解》,侵删。

对于一般的开发者,很少需要直接使用Java反射机制来完成功能开发,但是反射是很多框架例如如SpringMybatis实现的核心,反射虽小,能量却很大。

本文主要介绍反射相关的概念以及API的使用,关于反射的应用将在下一篇文章中介绍。

反射的介绍

反射Reflection)是Java在运行时(运行时)可以访问,检测和修改它本身状态或行为的一种能力,它允许运行中的Java程序获取自身的信息,并且可以操作类或对象的内部属性。

类类介绍:Java虚拟机为每个类型管理一个类对象,包含了与类有关的信息,当通过javac编译Java类文件时,生成的同名.class文件保存着该类的类对象,JVM加载一个类即是加载该.class文件。

Classjava.lang.reflect一起对反射提供了支持,java.lang.reflect包中最常用的几个类的关系如下:

java反射机制详解

其中最主要的三个类FieldMethodConstructor分别用于描述类的域,方法和构造器,它们有一个共同的父类AccessibleObject,它提供了访问控制检查的功能。

· 字段:描述类的域(属性),可以使用get()和set()方法读取和修改Field对象关联的细分;

· 方法:描述类的方法,可以使用invoke()方法调用与Method对象关联的方法;

· 构造函数:描述类的构造器,可以用构造函数创建新的对象。

下面将通过几个程序来学习Java反射机制

准备两个类用于实验

我们特别定义两个类,人员和雇员,其中雇员继承自人,并且各自都有一个私有的,受保护的,公共修饰的域(属性),雇员还有私有的,公共修饰的方法

public class Person {
    public String name; // 姓名 公有
    protected String age;   // 年龄 保护
    private String hobby;   // 爱好   私有

    public Person(String name, String age, String hobby) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
    public String getHobby() {
        return hobby;
    }
}

public class Employee extends Person {
    public static Integer totalNum = 0; // 员工数
    public int empNo;   // 员工编号 公有
    protected String position;  // 职位 保护
    private int salary; // 工资   私有

    public void sayHello() {
        System.out.println(String.format("Hello, 我是 %s, 今年 %s 岁, 爱好是%s, 我目前的工作是%s, 月入%s元\n", name, age, getHobby(), position, salary));
    }
    private void work() {
        System.out.println(String.format("My name is %s, 工作中勿扰.", name));
    }
    public Employee(String name, String age, String hobby, int empNo, String position, int salary) {
        super(name, age, hobby);
        this.empNo = empNo;
        this.position = position;
        this.salary = salary;
        Employee.totalNum++;
    }
}

获取类对象

获取Class对象的方式有一种:使用Class类的forName静态方法;直接获取某一个对象的class;调用某个对象的getClass()方法

public class ClassTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class c1 = Class.forName("reflect.Employee");   // 第1种,forName 方式获取Class对象
        Class c2 = Employee.class;      // 第2种,直接通过类获取Class对象
        Employee employee = new Employee("小明", "18", "写代码", 1, "Java攻城狮", 100000);
        Class c3 = employee.getClass();    // 第3种,通过调用对象的getClass()方法获取Class对象

        if (c1 == c2 && c1 == c3) {     // 可以通过 == 比较Class对象是否为同一个对象
            System.out.println("c1、c2、c3 为同一个对象");
            System.out.println(c1);     // class reflect.Employee
        }
    }
}

通过反射来创建实例

通过反射来生成对象主要有两种方式

· 使用Class对象的newInstance()方法来创建Class对象对应类的实例

· 先通过类对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例

public class NewInstanceTest {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class c = Date.class;
        Date date1 = (Date) c.newInstance();    // 第1种方式:使用Class对象的newInstance()方法来创建Class对象对应类的实例
        System.out.println(date1);      // Wed Dec 19 22:57:16 CST 2018

        long timestamp =date1.getTime();
        Constructor constructor = c.getConstructor(long.class); 
        Date date2 = (Date)constructor.newInstance(timestamp);  // 第2种方式:先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例
        System.out.println(date2);  // Wed Dec 19 22:57:16 CST 2018
    }
}

获取类的全部信息

上面我们定义了两个类,现在有个需求:获取员工的类名,构造器签名,所有的方法,所有的域(属性)和值,然后打印出来。

没错,猜对了,就是通过反射来获取这些类的信息,在上面介绍中我们知道JVM虚拟机为每个类型管理一个Class对象, 为了完成我们的需求,我们需要知道一些API如下: 

获取类信息的部分API

String getName() 获取这个Class的类名

Constructor[] getDeclaredConstructors()返回这个类的所有构造器的对象数组,包含保护和私有的构造器;相近的方法getConstructors()报道查看则这个类的所有公有构造器的对象数组,不包含保护和私有的构造器

Method[] getDeclaredMethods()返回这个类或接口的所有方法,包括保护和私有的方法,不包括超类的方法;相近的方法的getMethods()报道查看则这个类及其超类的公有方法的对象数组,不含保护和私有的方法

Field[] getDeclaredFields()返回这个类的所有域的对象数组,包括保护域和私有域,不包括超类的域;还有一个相近的API getFields(),这个报道查看类及其超类的公有域的对象数组,不含保护域和私有域 

int getModifiers()返回一个有用的描述字段,方法和构造函数的修饰符的整形数值,该数值代表的含义可通过Modifier这个类分析

Modifier 类 它提供了有关字段,方法和构造函数等的访问修饰符的信息,主要的方法有:toString(int修饰符)返回整形数值修饰符代表的修饰符的形式; isAbstract是否被抽象修饰; isVolatile是否被挥发性修饰; isPrivate是否为私有; isProtected是否为受保护; isPublic是否为public; isStatic是否为静态修饰;等等,见名知义。

打印类信息程序

public class ReflectionTest {
    public static void main(String[] args) throws ClassNotFoundException {
        String name;
        if (args.length > 0) {
            name = args[0];
        } else {
            Scanner in = new Scanner(System.in);
            System.out.println("输入一个类名(e.g. java.util.Date):"); // reflect.Employee
            name = in.next();
        }
        try {
            Class cl = Class.forName(name);
            Class superCl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print("class " + name);
            if (superCl != null && superCl != Object.class) {
                System.out.print(" extends " + superCl.getName());
            }
            System.out.println("\n{");

            printConstructors(cl); // 打印构造方法
            System.out.println();
            printMethods(cl);   // 打印方法
            System.out.println();
            printFields(cl);    // 打印属性
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    /**
     * 打印Class对象的所有构造方法
     */
    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String name = c.getName();
            System.out.print("  ");
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(name + "(");
            // 打印构造参数
            Class[] paramTypes = c.getParameterTypes();
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * 打印Class的所有方法
     */
    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();
        //Method[] methods = cl.getMethods();
        for (Method m : methods) {
            Class retType = m.getReturnType();  // 返回类型
            System.out.print("  ");
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(retType.getName() + " " + m.getName() + "(");
            Class[] paramTypes = m.getParameterTypes();
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * 打印Class的所有属性
     */
    public static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();
        for (Field f: fields) {
            Class type = f.getType();
            System.out.print("  ");
            String modifiers = Modifier.toString(f.getModifiers());
            if (modifiers.length()> 0) {
                System.out.print(modifiers + " ");
            }
            System.out.println(type.getName() + " " + f.getName() + ";");
        }
    }
}

运行程序,然后在控制台输入一个我们想分析的类的全名,例如如reflect.Employee,可得到下面的输出

输入一个类名(e.g. java.util.Date):
reflect.Employee
public class reflect.Employee extends reflect.Person
{
  private reflect.Employee(java.lang.String, java.lang.String, java.lang.String);
  public reflect.Employee(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String, int);

  public static void main([Ljava.lang.String;);
  public void sayHello();
  private void work();

  public static java.lang.Integer totalNum;
  public int empNo;
  protected java.lang.String position;
  private int salary;
}

上面的输出中我们得到的类的构造器,所有方法和所有的域(属性),包括修饰符,名称和参数类型都是正确的,看来反射机制能完成我们的需求。

小结一下,我们通过getDeclaredConstructors()获取构造器信息,通过getDeclaredMethods()获得方法信息,通过getDeclaredFields()获得域信息,再通过getModifiers()和Modifier类获得修饰符信息,汇总起来就得到了整个类别的类信息。

运行时查看对象数据域的实际内容

上面我们已经获取到了类的信息,现在又有一个需求:在运行时查看对象的数据域的实际值。

这个场景就像我们通过IDEA调试程序,设置断点拦截到程序后,查看某个对象的属性的值。

我们知道java反射机制提供了查看类信息的API,那么它应该也提供了查看Fielddomain实际值和设置Fielddomain实际值的API,没错,猜对了,确实有相关的API,但是有个疑问,有一些属性是私有修饰的私有域,这种是否也能直接查看和设置呢?看完下面的API就知道答案。

运行时查看对象数据域实际内容的相关API

Class getComponentType() 返回数组类里组件类型的类,如果不是多重类则返回null

boolean isArray() 返回这个类是否为副本,同类型的API还有is Annotation,isAsciiDigit,isEnum,isInstance,isInterface,isLocalClass,isPrimitive等

int Array.getLength(obj) 返回数组对象obj的长度

Object Array.get(obj, i) 获取数组对象下标为i的元素

boolean isPrimitive() 返回这个类是否为8种基本类型之一,即是否为布尔值,字节,字符,short,int,long,float和double等原始类型

Field getField(String name) 获取指定名称的域对象

AccessibleObject.setAccessible(fields, true) 当访问字段,方法和构造方法的时候,Java会执行访问检查,如果访问者没有权限将引发SecurityException,例如如访问者无法访问私有修饰的域的。通过设置setAccessible(true)可以取消Java的执行访问检查,这样访问者就获得了指定字段,方法或构造函数的访问权限

Class Field.getType() 返回一个类对象,它标识了此字段对象所表示的段的声明类型

Object Field.get(Object obj) 获取对象对象上当前域对象表示的属性的实际值,获取到的是一个对象对象,实际使用中还需要转换成实际的类型,或者可以通过getByte(),getChar,getInt()等直接获取具体类型的值

void Field.set(Object obj, Object value) 设置obj对象上当前域表示的属性的实际值

查看对象数据域实际内容程序

了解完上述相关API之后,我们敲出下面的程序来验证

public class ObjectAnalyzer {
    private ArrayList<Object> visited = new ArrayList<>();

    public String toString(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (visited.contains(obj)) {    // 如果该对象已经处理过,则不再处理
            return "...";
        }
        visited.add(obj);

        Class cl = obj.getClass(); // 获取Class对象
        if (cl == String.class) {   // 如果是String类型则直接转为String
            return (String) obj;
        }
        if (cl.isArray()) {        // 如果是数组
            String r = cl.getComponentType() + "[]{\n";     // 数组的元素的类型
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {   // 不是数组的第一个元素加逗号和换行,显示更加美观
                    r += ",\n";
                }
                r += "\t";
                Object val = Array.get(obj, i);
                if (cl.getComponentType().isPrimitive()) { // Class为8种基本类型的时候为 true,直接输出
                    r += val;
                } else {
                    r += toString(val); // 不是8中基本类型时,说明是类,递归调用toString
                }
            }
            return r + "\n}";
        }
        // 既不是String,也不是数组时,输出该对象的类型和属性值
        String r = cl.getName();
        do {
            r += "[";
            Field[] fields = cl.getDeclaredFields();    // 获取该类自己定义的所有域,包括私有的,不包括父类的
            AccessibleObject.setAccessible(fields, true); // 访问私有的属性,需要打开这个设置,否则会报非法访问异常
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) { // 通过 Modifier 可获取该域的修饰符,这里判断是否为 static
                    if (!r.endsWith("[")) {
                        r += ",";
                    }
                    r += f.getName() + "=";     // 域名称
                    try {
                        Class t = f.getType();  // 域(属性)的类型
                        Object val = f.get(obj);   // 获取obj对象上该域的实际值
                        if (t.isPrimitive()) {     // 如果类型为8种基本类型,则直接输出
                            r += val;
                        } else {
                            r += toString(val);     // 不是8种基本类型,递归调用toString
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            cl = cl.getSuperclass(); // 继续打印超类的类信息
        } while (cl != null);
        return r;
    }
}

测试验证结果

接下来验证一下获取数据域实际值是否正确,分别打印数组,自定义类的对象的实际值

public class ObjectAnalyzerTest {
    public static void main(String[] args) {
        int size = 4;
        ArrayList<Integer> squares = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            squares.add(i * i);
        }
        ObjectAnalyzer objectAnalyzer = new ObjectAnalyzer(); // 创建一个上面定义的分析类ObjectAnalyzer的对象
        System.out.println(objectAnalyzer.toString(squares)); // 分析ArrayList<Integer>对象的实际值

        Employee employee = new Employee("小明", "18", "爱好写代码", 1, "Java攻城狮", 100); // 分析自定义类Employee的对象的实际值
        System.out.println(objectAnalyzer.toString(employee));
    }
}

输出如下

java.util.ArrayList[elementData=class java.lang.Object[]{
    java.lang.Integer[value=0][][],
    java.lang.Integer[value=1][][],
    java.lang.Integer[value=4][][],
    java.lang.Integer[value=9][][]
},size=4][modCount=4][][]
reflect.Employee[empNo=1,position=Java攻城狮,salary=100][name=小明,age=18,hobby=爱好写代码][]

其中ArrayList<Integer>打印了类名和5个元素的类型和值,Employee打印了类名,自己定义的3个基本类型的属性的实际值,和父类Person的3个基本类型的属性的实际值。

需要注意的是,位置,年龄是受保护的域,工资,爱好是私有的私有域,Java的安全机制只允许查看任意对象有该域,但可以替换它们的值。

程序中是通过AccessibleObject.setAccessible(fields, true)将域设置为了可访问,取消了Java的执行访问检查,因此可以访问,如果不加会报异常IllegalAccessException。

小结一下,我们通过setAccessible(true)绕过了Java执行访问检查,因此能够访问私有域,通过Field.getType()获得了属性的声明类型,通过了Field.get(Object obj)获得了该域属性的实际值,还有一个没用上的Field.set(Object obj,Object value)设置域属性的实际值。

调用任意方法

上面我们已经获取了类的构造器,方法,域,查看和设置了域的实际值,那么不是还可以在调用对象的方法呢?

嘿嘿,又猜对了,机智,类的方法信息,获取都获取了,当然就要调用一下,来都来了。

上面查看Field的实际值是通过Field类的get()方法,与之类似,Method调用方法是通过Method类的invoke方法。

调用任意方法相关的API

Method getMethod(String name, Class<?>... parameterTypes) 获取指定的方法,参数名称为要获取的方法名称,parameterTypes为指定方法的参数的类,由于可能存在多个同名的重载方法,所以只有提供正确的parameterTypes才能准确地获取到指定的Method

Object invoke(Object obj, Object... args) 执行方法,第一个参数执行该方法的对象,如果是静态修饰的类方法,则传null即可;后面是传给该方法执行的具体的参数值

调用任意方法程序

public class MethodTableTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Employee employee = new Employee("小明", "18", "写代码", 1, "Java攻城狮", 100000);
        Method sayHello = employee.getClass().getMethod("sayHello");
        System.out.println(sayHello);   // 打印 sayHello 的方法信息
        sayHello.invoke(employee);      // 让 employee 执行 sayHello 方法

        double x = 3.0;
        Method square = MethodTableTest.class.getMethod("square", double.class);  // 获取 MethodTableTest 的square方法
        double y1 = (double) square.invoke(null, x);    // 调用类方法 square 求平方,方法参数 x 
        System.out.printf("square    %-10.4f -> %10.4f%n", x, y1);

        Method sqrt = Math.class.getMethod("sqrt", double.class);   // 获取 Math 的 sqrt 方法
        double y2 = (double) sqrt.invoke(null, x);  // 调用类方法 sqrt 求根,方法参数 x 
        System.out.printf("sqrt      %-10.4f -> %10.4f%n", x, y2);
    }

    // static静态方法 计算乘方
    public static double square(double x) {
        return x * x;
    }
}

执行结果

public void reflect.Employee.sayHello()
Hello, 我是 小明, 今年 18 岁, 爱好是写代码, 我目前的工作是Java攻城狮, 月入100000元

square    3.0000     ->     9.0000
sqrt      3.0000     ->     1.7321

相信大家都看懂啦,通过getMethod()获取指定的方法,再调用Method.invoke()执行该方法。

反射的优缺点

(此段引用自CyC2018 / CS-注意事项)

反射的优点

· 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。

· 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员。可视化开发环境(如IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。

· 调试器和测试工具:调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的API定义,以确保进行测试中有更多的代码覆盖率。

反射的缺点

如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。

· 性能开销:反射涉及了动态类型的解析,所以JVM无法对这些代码进行优化因此,反射操作的效率要比那些非反射操作低得多我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

· 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。

· 内部暴露:由于反射允许代码执行某些在正常情况下不被允许的操作(因此访问私有的属性和方法),因此使用反射可能会导致意料之外的其他,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也发生变化。

(参考: 《 Java核心技术》卷一 )

相关阅读 >>

Java 语言有哪些特点?

Java中将list集合用逗号隔开进行拼接成字符串

Java 中的 io 流

Java 和 c++ 的区别 ?

Java判断一个字符串是否为纯数字

Java 中的异常处理

Java 动态代理详解

hashcode 与 equals

Java 中获取用键盘输入常用的两种方法

Java 教程

更多相关阅读请进入《Java》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...