Java 高级:反射知识总结

一、背景


要理解反射,首先要知道它产生的背景。

在 Java 中,正常情况下我们只需要 new 某个类来使用就行了,但是如果想在运行时灵活创建某个类怎么办?想要使用某个类但是并没有被 JVM 加载怎么办?

答案就是利用 反射,这个机制可以帮助我们在运行期需要的时候去加载创建某个类,从而使用该类的方法。

这里引用 知乎Kira 的回答可能更容易理解:

假如你写了一段代码:Object o=new Object();运行了起来!
首先JVM会启动,你的代码会编译成一个.class文件,然后被类加载器加载进jvm的内存中,你的类Object加载到方法区中,创建了Object类的class对象到堆中,注意这个不是new出来的对象,而是类的类型对象,每个类只有一个class对象,作为方法区类的数据结构的接口。
jvm创建对象前,会先检查类是否加载,寻找类对应的class对象,若加载好,则为你的对象分配内存,初始化也就是代码:new Object()。
上面的流程就是你自己写好的代码扔给jvm去跑,跑完就over了,jvm关闭,你的程序也停止了。

相当于这些代码都是写 "死" 的,但是在运行时要使用则需要利用反射通过包名灵活地加载某些类。

二、反射的组件


实现反射的一些主要类都处于 java.lang 包下:

  • Class:代表一个类;
  • Field:代表类的成员变量(属性);
  • Method:代表类的方法;
  • Constructor:代表构造器(构造函数、构造方法);
  • Array:提供动态创建数组,以及访问数组元素的方法;
  • Package:存放路径,包名;

接下来看一下怎么获取这些组件。

类对象 Class

  • 通过类型获取;
Class c = String.class;
  • 通过对象获取:
Class c = o.getClass();
名称 作用
getName() 返回String形式的该类的名称。
newInstance() 根据某个Class对象产生其对应类的实例,它调用的是此类的默认构造方法(没有默认无参构造器会报错)
getClassLoader() 返回该Class对象对应的类的类加载器。
getSuperClass() 返回某子类所对应的直接父类所对应的Class对象
isArray() 判定此Class对象所对应的是否是一个数组对象
getComponentType() 如果当前类表示一个数组,则返回表示该数组组件的 Class 对象,否则返回 null。
getConstructor(Class[]) 返回当前 Class 对象表示的类的指定的公有构造子对象。
getConstructors() 返回当前 Class 对象表示的类的所有公有构造子对象数组。
getDeclaredConstructor(Class[]) 返回当前 Class 对象表示的类的指定已说明的一个构造子对象。
getDeclaredConstructors() 返回当前 Class 对象表示的类的所有已说明的构造子对象数组。
getDeclaredField(String) 返回当前 Class 对象表示的类或接口的指定已说明的一个域对象。
getDeclaredFields() 返回当前 Class 对象表示的类或接口的所有已说明的域对象数组。
getDeclaredMethod(String, Class[]) 返回当前 Class 对象表示的类或接口的指定已说明的一个方法对象。
getDeclaredMethods() 返回 Class 对象表示的类或接口的所有已说明的方法数组。
getField(String) 返回当前 Class 对象表示的类或接口的指定的公有成员域对象。
getFields() 返回当前 Class 对象表示的类或接口的所有可访问的公有域对象数组。
getInterfaces() 返回当前对象表示的类或接口实现的接口。
getMethod(String, Class[]) 返回当前 Class 对象表示的类或接口的指定的公有成员方法对象。
getMethods() 返回当前 Class 对象表示的类或接口的所有公有成员方法对象数组,包括已声明的和从父类继承的方法。
isInstance(Object) 此方法是 Java 语言 instanceof 操作的动态等价方法。
isInterface() 判定指定的 Class 对象是否表示一个接口类型
isPrimitive() 判定指定的 Class 对象是否表示一个 Java 的基类型。
newInstance() 创建类的新实例

构造器 Constructor

首先使用 Class 对象获取 Constructor

  • getConstructors()
  • getConstructor(Class<?>…parameterTypes)
  • getDeclaredConstructors()
  • getDeclaredConstructor(Class<?>...parameterTypes)

然后就可以使用 Constructor 的下列方法:

名称 作用
isVarArgs() 查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回false
getParameterTypes() 按照声明顺序以 Class 数组的形式获取该构造方法各个参数的类型
getExceptionTypes() 以 Class 数组的形式获取该构造方法可能抛出的异常类型
newInstance(Object … initargs) 通过该构造方法利用指定参数创建一个该类型的对象,如果未设置参数则表示采用默认无参的构造方法
setAccessiable(boolean flag) 如果该构造方法的权限为 private,默认为不允许通过反射利用 netlnstance()方法创建对象。如果先执行该方法,并将入口参数设置为 true,则允许创建对象
getModifiers() 获得可以解析出该构造方法所采用修饰符的整数

Modifier 用来获取修饰符信息,比如 isStatic(int mod) 返回是否静态、isPublic(int mod) 是否公共。不只是构造器类型含有此方法,函数、属性都有此方法。

方法 Method

名称 作用
getName() 获取该方法的名称
getParameterType() 按照声明顺序以 Class 数组的形式返回该方法各个参数的类型
getReturnType() 以 Class 对象的形式获得该方法的返回值类型
getExceptionTypes() 以 Class 数组的形式获得该方法可能抛出的异常类型
invoke(Object obj,Object...args) 利用 args 参数执行指定对象 obj 中的该方法,返回值为 Object 类型
isVarArgs() 查看该方法是否允许带有可变数量的参数,如果允许返回 true,否则返回 false
getModifiers() 获得可以解析出该方法所采用修饰符的整数

属性、字段 Field

名称 作用
getName() 获得该成员变量的名称
getType() 获取表示该成员变量的 Class 对象
get(Object obj) 获得指定对象 obj 中成员变量的值,返回值为 Object 类型
set(Object obj, Object value) 将指定对象 obj 中成员变量的值设置为 value
getlnt(0bject obj) 获得指定对象 obj 中成员类型为 int 的成员变量的值
setlnt(0bject obj, int i) 将指定对象 obj 中成员变量的值设置为 i
setFloat(Object obj, float f) 将指定对象 obj 中成员变量的值设置为 f
getBoolean(Object obj) 获得指定对象 obj 中成员类型为 boolean 的成员变量的值
setBoolean(Object obj, boolean b) 将指定对象 obj 中成员变量的值设置为 b
getFloat(Object obj) 获得指定对象 obj 中成员类型为 float 的成员变量的值
setAccessible(boolean flag) 此方法可以设置是否忽略权限直接访问 private 等私有权限的成员变量
getModifiers() 获得可以解析出该方法所采用修饰符的整数

数组相关 Array

名称 作用
newInstance(Class<?> componentType, int length) 创建一个 componentType 类型的数组,设定长度
getLength(Object array) 获取数组长度
get(Object array, int index) 获取数组指定下标位置的数据
set(Object array, int index, Object value) 设置数组指定下标位置的数据

三、使用


获取组件

接下来写一个简单的小例子来看下反射的使用,首先准备一个 Student 类:

package com.sky.test.ref;

public class Student {

    private int privateAge;
    public int publicAge;

    public void publicStudy(){
    }

    private void privateStudy(){
    }
}

很简单的类,拥有一个私有、一个公共属性,一个私有、一个公共方法,接下来使用反射获取这些信息。

public static void main(String[] args) {

    try {
        // 根据包名找到类型
        Class<?> classType = Class.forName("com.sky.test.ref.Student");

        // 获取所有属性
        Field[] fields = classType.getFields();
        Field[] fieldsD = classType.getDeclaredFields();
        for (Field f : fields) {
            System.out.println("getFields 获取到的属性:" + f.getName());
            // getFields 获取到的属性:publicAge
        }

        for (Field f : fieldsD) {
            System.out.println("getDeclaredFields 获取到的属性:" + f.getName());
            // getDeclaredFields 获取到的属性:privateAge
            // getDeclaredFields 获取到的属性:publicAge
        }

        // 获取该类的所有方法
        Method[] methods = classType.getMethods();
        Method[] methodsD = classType.getDeclaredMethods();
        for (Method m : methods) {
            System.out.println("getMethods 获取到的方法:" + m.getName());
            // getMethods 获取到的方法:publicStudy
            // getMethods 获取到的方法:wait
            // getMethods 获取到的方法:wait
            // getMethods 获取到的方法:wait
            // getMethods 获取到的方法:equals
            // getMethods 获取到的方法:toString
            // getMethods 获取到的方法:hashCode
            // getMethods 获取到的方法:getClass
            // getMethods 获取到的方法:notify
            // getMethods 获取到的方法:notifyAll
        }

        for (Method m : methodsD) {
            System.out.println("getDeclaredMethods 获取到的方法:" + m.getName());
            // getDeclaredMethods 获取到的方法:publicStudy
            // getDeclaredMethods 获取到的方法:privateStudy
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

}
  • Class.forName() 方法传入包名,以找到想要使用的类,找不到会抛 ClassNotFoundException 异常;
  • ClassgetFields() 方法:获取所有 公共的(public) 的属性,包含父类的 public 属性(但不包含父类非 public 属性);
  • ClassgetDeclaredFields() 方法:获取本类所有的属性,但不包含父类的属性;
  • ClassgetMethods() 方法:获取所有 公共的(public) 的方法,包含父类的 public 方法(但不包含父类非 public 方法);
  • ClassgettDeclaredMethods() 方法:获取本类所有的方法,但不包含父类的方法;

简单使用

已经成功获取到这些方法和属性了,接下来就是使用通过反射获取到的属性和方法了。但是首先要做的是能把修改的信息展示出来,把 Student 类简单修改下:

public class Student extends SuperStudent {

    private int privateAge;
    public int publicAge;

    public void publicStudy(int hour) {
        System.out.println("设置年龄为" + publicAge);
        System.out.println("学习了" + hour + "小时");
    }

    private void privateStudy() {
    }
}

只是给 publicStudy() 方法增加了 int 类型的参数,并打印了两个字段:publicAge、hour。接下来就可以使用反射来修改里面的信息了。

public static void main(String[] args) {

    try {
        // 1. 根据包名找到类型
        Class<?> classType = Class.forName("com.sky.test.ref.Student");
        // 创建实例,修改该对象的信息
        Object o = classType.newInstance();
        // 2. 获取 年龄 字段
        Field field = classType.getDeclaredField("publicAge");
        // 设置年龄 publicAge 属性:第一个参数为要修改的对象,第二个参数为数值
        field.set(o, 1);
        // 3. 获取 publicStudy 方法,第二个参数传入参数类型
        Method method = classType.getDeclaredMethod("publicStudy", int.class);
        // 调用 publicStudy 方法
        method.invoke(o, 3);
        
    } catch (ClassNotFoundException
            | NoSuchFieldException
            | NoSuchMethodException | IllegalAccessException
            | InstantiationException | InvocationTargetException
            e) {
        e.printStackTrace();
    }

}
  1. 首先要做的是找到该类型,并创建该类型的对象。修改数据首先得有对象吧,然后内存空间有它的地方吧,不然修改啥。
  2. 然后就是获取字段了,我们要做的是修改年龄 publicAge 字段:获取该字段的 Field 对象,使用 set() 传入 对象 和要修改的值,这样就 ok 了。
  3. 类似的方法获取 publicStudy() 方法,调用 invoke() 方法传参激活打印。特别注意: 第二个参数是当前方法的参数类型,如果传的不对直接给你报 NoSuchMethodException。而且注意 int.classInteger.class 竟然不是一个东西,传 Integer 还是会报错...

这样就完成了普通的使用。

特别注意:权限问题

反射使用引入了权限机制,私有的属性已经方法直接反射调用的话会抛出异常。

Field fieldPrivate = classType.getDeclaredField("privateAge");
fieldPrivate.set(o,2); // 会抛出 java.lang.IllegalAccessException 异常

如果想要使用的话,需要设置属性或方法等的访问权限:

Field fieldPrivate = classType.getDeclaredField("privateAge");
fieldPrivate.setAccessible(true);
fieldPrivate.set(o,2);

注解使用

注解一般也是利用反射来使用的,大概过程就是利用反射获取 Class 类型上面的注解,然后执行相应逻辑。

之前写过一篇文章有记录,在此不再赘述。

谈谈你对注解的理解 第三节

远程方法运用反射

摘自 Java在远程方法调用中运用反射机制

  • 服务端:HelloService
  • 客户端:SimpleClient
  • 通信信息:Call

客户端生成 Call 对象,指定要调用的类、对象以及方法参数等信息,通过流的形式发生给服务端。

public class Call implements Serializable {
    private static final long serialVersionUID = 6659953547331194808L;
    private String className; // 表示类名或接口名
    private String methodName; // 表示方法名
    private Class[] paramTypes; // 表示方法参数类型
    private Object[] params; // 表示方法参数值
    // 表示方法的执行结果
    // 如果方法正常执行,则result为方法返回值,如果方法抛出异常,那么result为该异常。
    private Object result;
    ...
}

服务端处理过将结果再以流的形式返回给客户端。

远程反射

反射操作数组

需求: 创建一个数组,通过反射获取数组的类型,然后创建一个新长度的新数组,拷贝旧数组的内容。

为了看出效果,首先为 Student 类增加一个静态的创建方法:

public class Student extends SuperStudent {
    public int publicAge;
    ...
    public static Student newInstance(){
        Student student = new Student();
        student.publicAge = 8;
        return student;
    }
}

使用此方法创建实例,给内部的年龄字段一个初始值 8。接下来就创建一个 Student 的数组,并利用反射进行创建和复制。

public static void main(String[] args) {

    // 1. 创建数组,里面存放一个 Student 对象
    Student[] students = new Student[]{Student.newInstance()};
    // 2. 获取元素类型
    Class<?> classType = students.getClass().getComponentType();

    // 3. 创建新数组
    Object[] objects = (Object[]) Array.newInstance(classType, Array.getLength(students));
    // 4. 进行 copy:原数组,拷贝起始,新数组,拷贝起始,拷贝长度
    System.arraycopy(students, 0, objects, 0, Array.getLength(students));

    // 5. 从新数组获取第一个元素
    Student s = (Student)Array.get(objects, 0);
    System.out.println("拷贝完成,第一个数据年龄: " + s.publicAge);
    // 拷贝完成,第一个数据年龄: 8
}

需要注意的是一些方法的使用:

  • getClass()getComponentType() 的区别:
    getClass()用于获取对象的类型,比如这里获取数组的是 Student[] 类型;
    getComponentType() 获取数组元素类型,比如这里获取到的是 Student 类型。

  • Array.newInstance() 可以帮助我们动态创建数组,可以从别的地方传递来类型和长度,即可完成创建。

反射操作泛型

知识储备:

  • Type:所有类型的公共父接口,Class 就是 Type 的子类之一,以下是该接口的子类型:
    • ParameterizedType:参数化类型,可以理解为泛型;
    • TypeVariable:类型变量,也就是平时定义的 T t、K k 等类似的变量;
    • GenericArrayType:泛型数组类型,T[] 这种类型;
    • WildcardType:通配符类型,<?>, <? Extends String> 这种。

小栗子:

依旧是使用上面的 Student 类,给它增加一个包含泛型的方法:

public void setStudents(List<Student> list){
}

接下来就是获取泛型:

public static void main(String[] args) {
    try {
        Method method = Student.class.getMethod("setStudents", List.class);
        // 获取方法参数类型
        Type[] t = method.getGenericParameterTypes();

        for (Type type : t) {
            System.out.println("参数类型:" + type);// 泛型类型:java.util.List<com.sky.test.ref.Student>
            if (type instanceof ParameterizedType) {
                Type[] real = ((ParameterizedType) type).getActualTypeArguments();
                for (Type genericType : real) {
                    System.out.println("泛型类型:" + genericType);
                    // 泛型类型:class com.sky.test.ref.Student
                }
            }
        }

    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
}

这只是利用反射获取泛型的一个很小的例子,可以使用不同的方法获取 接口、类等的参数、返回值中的泛型。

说起来为什么会把泛型擦除掉,是因为泛型本来不是 Java 中的东西(可能是抄来的)。如果要把泛型加入字节码,就需要修改字节码指令集。我们都知道新增简单修改难,这字节码指令用了多少年了,要改基本不肯能了。

所以把泛型擦除之后呢,又需要一些类型来表示到底是哪种泛型,于是就有了上面那些 Type 的子接口。

四、反射影响性能

这里引用文章 大家都说 Java 反射效率低,你知道原因在哪里么 的结论:

  1. Method#invoke 方法会对参数做封装和解封操作
  2. 需要检查方法可见性
  3. 需要校验参数
  4. 反射方法难以内联
  5. JIT 无法优化

具体可以参考该文章,自此本文结束。

参考:

Java反射机制
Java 反射操作数组

推荐阅读更多精彩内容