《一步到位》——全面理解Java中的反射

一、简介

通过反射,Java代码可以发现有关已加载类的字段,方法和构造函数的信息,并可以在安全限制内对这些字段,方法和构造函数进行操作。

简而言之,你可以在运行状态中通过反射机制做到:

  • 对于任意一个类,都能够知道这个类的所有属性和方法;
  • 对于任意一个对象,都能够调用它的任意一个方法和属性;

这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

使用Java反射主要涉及两个类(接口)Class, Member,如果把这两个类搞清楚了,反射基本就可以了。

二、Class类

  • 对于每一种类,Java虚拟机都会初始化出一个Class类型的实例,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象,并且这个Class对象会被保存在同名.class文件里。
  • 当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。

比如创建编译一个Shapes类,那么,JVM就会创建一个Shapes对应Class类的Class实例,该Class实例保存了Shapes类相关的类型信息,包括属性,方法,构造方法等等,通过这个Class实例可以在运行时访问Shapes对象的属性和方法等。另外通过Class类还可以创建出一个新的Shapes对象。这就是反射能够实现的原因,可以说Class是反射操作的基础。

需要特别注意的是,每个class(注意class是小写,代表普通类)类,无论创建多少个实例对象,在JVM中都对应同一个Class对象。

简单一个例子:
Animal.java

public class Animal {
    private String name;
    private int age;

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

    @Override
    public String toString() {
        return "Animal : name = " + name + " age = " + age;
    }
}

测试类:TestReflection.java

public class TestReflection {

    public static void main(String[] args) {

        // 获取Animal类的Class对象
        Class clazz = Animal.class;
        try {
            // 通过Class对象反射获取Animal类的构造方法
            Constructor constructor = clazz.getConstructor(String.class, int.class);

            // 调用构造方法获取Animal实例
            Animal animal = (Animal) constructor.newInstance("jsai",3);

            // 将构造出来的Animal对象打印出来
            System.err.println(animal.toString());

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

结果:
在这里插入图片描述

如何获取Class

说Class是反射能够实现的基础的另一个原因是:Java反射包java.lang.reflect中的所有类都没有public构造方法,要想获得这些类实例,只能通过Class类获取。所以说如果想使用反射,必须得获得Class对象。

一般获取Class对象的方法:

(1)通过对象的getClass()方法(常用)

// 返回字符串的Class对象
Class c = "str".getClass();

// 返回与枚举类型E对应的类
enum E { A, B }
Class c = A.getClass();

// 返回与组件类型为byte的数组对应的类.
byte[] bytes = new byte[1024];
Class c = bytes.getClass();

// 返回与java.util.HashSet对应的类.
Set<String> s = new HashSet<String>();
Class c = s.getClass();

但是,然而对于基本类型无法使用这种方法。

boolean b;
Class c = b.getClass();   // 编译出错

(2)通过类的类型获取Class对象,基本类型同样可以使用这种方法。即:类型.class(常用)

Class c = boolean.class;
Class c = String.class;

(3)通过Class.forName()(常用)

通过类的全限定名获取Class对象, 基本类型无法使用此方法

Class c = Class.forName("java.lang.String");

(4)基本类型和void 类型的包装类可以使用TYPE字段获取

Class c = Double.TYPE;   //等价于 double.class.
Class c = Void.TYPE;

(5)通过反射方法获取Class对象

有一些反射方法可以获取Class对象,但前提是你已经获取了一个Class对象。比如说你已经获取了一个类的Class对象,就可以通过反射方法获取这个类的父类的Class对象。
例如:

Class.getSuperclass()
Class.getClasses()
Class.getDeclaredClasses()
Class.getDeclaringClass()
Class.getEnclosingClass()
java.lang.reflect.Field.getDeclaringClass()
java.lang.reflect.Method.getDeclaringClass()
java.lang.reflect.Constructor.getDeclaringClass()

通过Class获取类修饰符和类型

在这里插入图片描述
下面我们就以HashMap为例,通过一个Demo来说明如何获取这些信息

public static void testGetMapInfo(){

        Class<?> clazz = HashMap.class;

        // 获取类名
        System.out.println("Class: "+clazz.getCanonicalName());

        // 获取类限定符
        System.out.println("Modifiers: "+ Modifier.toString(clazz.getModifiers()));

        // 获取类泛型信息
        TypeVariable[] typeParameters = clazz.getTypeParameters();
        if (typeParameters.length != 0){
            StringBuffer param = new StringBuffer("Parameters: ");
            for (TypeVariable tv : typeParameters){
                param.append(tv.getName());
                param.append("  ");
            }
            System.out.println(param.toString());
        }else {
            System.out.println("-- No Type Parameters --");
        }

        // 获取类实现的所有接口
        Type[] intfs = clazz.getGenericInterfaces();
        if (intfs.length != 0) {
            StringBuilder interfaces = new StringBuilder("Implemented Interfaces: ");
            for (Type intf : intfs){
                interfaces.append(intf.toString());
                interfaces.append("  ");
            }
            System.out.println(interfaces.toString());
        } else {
            System.out.println("-- No Implemented Interfaces --");
        }

        // 获取类继承数上的所有父类
        List<Class> superClass = new ArrayList<>();
        printAncestor(clazz, superClass);
        if (superClass.size() != 0){
            StringBuffer inheritance = new StringBuffer("Inheritance Path: ");
            for (Class<?> c : superClass){
                inheritance.append(c.getCanonicalName());
                inheritance.append("  ");
            }
            System.out.println(inheritance.toString());
        }else {
            System.out.println("-- No Super Classes --%n%n");
        }

        // 获取类的注解(只能获取到 RUNTIME 类型的注解)
        Annotation[] ann = clazz.getAnnotations();
        if (ann.length!=0){
            StringBuilder annotation = new StringBuilder("Annotations : ");
            for (Annotation a : ann){
                annotation.append(a.toString());
                annotation.append(" ");
            }
            System.out.println(annotation.toString());
        }else {
            System.out.println("-- No Annotations --%n%n");
        }
    }

    // 递归向上查找父类
    private static void printAncestor(Class<?> c, List<Class> l) {
        Class<?> ancestor = c.getSuperclass();
        if (ancestor != null) {
            l.add(ancestor);
            printAncestor(ancestor, l);
        }
    }

结果:在这里插入图片描述

三、Member类

对于Member接口可能会有人不清楚是干什么的,但如果提到实现它的三个实现类,估计用过反射的人都能知道。我们知道类成员主要包括构造函数变量方法,Java中的操作基本都和这三者相关,而Member的这三个实现类就分别对应他们。

java.lang.reflect.Field:对应类变量
java.lang.reflect.Method:对应类方法
java.lang.reflect.Constructor:对应类构造函数

反射就是通过这三个类才能在运行时改变对象状态。下面就让我们通过一些例子来说明如何通过反射操作它们。
首先写一个测试类:

public class Cat {

    public static final String TAG = Cat.class.getSimpleName();

    private String name;

    @Deprecated
    public int age;

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

    public String getName(){
        return name;
    }

    public void eat(String food){
        System.err.println("eat food " + food);
    }

    public void eat(String... foods){
        StringBuilder s = new StringBuilder();
        for(String food : foods){
            s.append(food);
            s.append(" ");
        }
        System.err.println("eat food " + s.toString());
    }

    public void sleep(){
        System.err.println("sleep");
    }

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

1、Field类

通过Field你可以访问给定对象的类变量,包括获取变量的类型修饰符注解变量名变量的值或者重新设置变量值即使变量是private的

(1)获取Field

Class提供了4种方法获得给定类的Field:

方法 解释
getFields() 获取所有的public变量
getDeclaredFields() 获取所有声明的变量(包括private)
getField(String name) 获取指定的变量(只能获得public的)
getDeclaredField(String name) 获取指定的变量(包括private)
(2)获取变量类型、修饰符、注解
public static void testField1(){
        Class clazz = Cat.class;

        Field[] fields = clazz.getDeclaredFields();

        for (Field field:fields){
            StringBuilder builder = new StringBuilder();
            // 获取名称
            builder.append("filed name = "+field.getName());

            // 获取类型
            builder.append(" type = "+field.getType());

            // 获取修饰符
            builder.append(" modifiers = "+ Modifier.toString(field.getModifiers()));

            // 获取注解
            Annotation[] ann = field.getAnnotations();
            if (ann.length != 0) {
                builder.append(" annotations = ");
                for (Annotation a : ann){
                    builder.append(a.toString());
                    builder.append(" ");
                }
            } else {
                builder.append("  -- No Annotations --");
            }
            System.err.println(builder.toString());
        }
    }

结果:
在这里插入图片描述

(3)获取、设置变量值
public static void testField2(){
        Cat cat = new Cat("Tom", 2);
        Class clazz = cat.getClass();

        try {
            // 注意获取private变量时,需要用getDeclaredField
            Field fieldName = clazz.getDeclaredField("name");
            Field fieldAge = clazz.getField("age");
            System.err.println(fieldName+" --- "+fieldAge);

            // fieldName.setAccessible(true);
            // 反射获取名字, 年龄
            String name = (String) fieldName.get(cat);
            int age = fieldAge.getInt(cat);
            System.out.println("before set, Cat name = " + name + " age = " + age);

            // 反射重新set名字和年龄
            fieldName.set(cat, "Timmy");
            fieldAge.setInt(cat, 3);
            System.err.println("after set, Cat " + cat.toString());

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

如果直接运行上述代码,会出现java.lang.IllegalAccessException异常。说我们没有权限操作变量name;
原来name变量是private,Java运行时会进行访问权限检查,private类型的变量无法进行直接访问,刚刚进行的反射操作并没有打破这种封装,所以我们依然没有权限对private属性进行直接访问。

解决办法:
java.lang.reflect.AccessibleObjectAccessibleObject为我们提供了一个方法 setAccessible(boolean flag),该方法的作用就是可以取消 Java 语言访问权限检查。所以任何继承AccessibleObject的类的对象都可以使用该方法取消 Java 语言访问权限检查。(final类型变量也可以通过这种办法访问)

Field正是AccessibleObject的子类,因此,只要在访问私有变量前调用field.setAccessible(true)就可以了,即加上

	fieldName.setAccessible(true);
    // 反射获取名字, 年龄
    String name = (String) fieldName.get(cat);
    int age = fieldAge.getInt(cat);

结果:
在这里插入图片描述
注意:
FieldMethodConstructor都是继承AccessibleObject,所以如果遇到私有方法和私有构造函数无法访问,记得处理方法一样。

2、Method类

通过反射访问对象的方法。

(1)获取Method

Class依然提供了4种方法获取Method

方法 解释
getMethods() 获取所有的public方法
getDeclaredMethods() 获取所有声明的方法
getMethod(String name, Class<?>... parameterTypes) 根据方法名获取指定的public方法
getDeclaredMethod(String name, Class<?>... parameterTypes) 根据方法名获得指定的方法

参数name:方法名,
参数parameterTypes:方法的参数类型
如 getDeclaredMethod(“eat”, String.class)
获取带参数方法时,如果参数类型错误会报NoSuchMethodException,对于参数是泛型的情况,泛型须当成Object处理(Object.class)

(2)获取方法返回类型
方法 解释
getReturnType() 获取目标方法返回类型对应的Class对象
getGenericReturnType() 获取目标方法返回类型对应的Type对象

这两个方法的区别?

  1. getReturnType()返回类型为Class,getGenericReturnType()返回类型为Type; Class实现Type。
  2. 返回值为普通简单类型,如Object, int, String等,getGenericReturnType()返回值和getReturnType()一样

例如:public String function1()
那么各自返回值为:
getReturnType() : class java.lang.String
getGenericReturnType() : class java.lang.String

  1. 返回值为泛型

例如:public T function2()
那么各自返回值为:
getReturnType() : class java.lang.Object
getGenericReturnType() : T

  1. 返回值为参数化类型

例如:public Class<String> function3()
那么各自返回值为:
getReturnType() : class java.lang.Class
getGenericReturnType() : java.lang.Class<java.lang.String>

反射中所有形如getGenericXXX()的方法规则都与上面所述类似。

(3)获取方法参数类型
方法 解释
getParameterTypes() 获取目标方法各参数类型对应的Class对象
getGenericParameterTypes() 获取目标方法各参数类型对应的Type对象
(4)获取方法声明抛出的异常的类型
方法 解释
getExceptionTypes() 获取目标方法抛出的异常类型对应的Class对象
getGenericExceptionTypes() 获取目标方法抛出的异常类型对应的Type对象
(5)获取方法参数名称

.class文件中默认不存储方法参数名称,如果想要获取方法参数名称,需要在编译的时候加上-parameters参数。(构造方法的参数获取方法同样)

(6)获取方法修饰符

方法与Field获取属性权限等类似

method.getModifiers();
(7)通过反射调用方法

反射通过Method的invoke()方法来调用目标方法。

  • 第一个参数为需要调用的目标类对象,如果方法为static的,则该参数为null。
  • 后面的参数都为目标方法的参数值,顺序与目标方法声明中的参数顺序一致。
public native Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

注意:如果方法是private的,可以使用method.setAccessible(true)方法绕过权限检查

public static void testField3(){

        Class<?> clazz = Cat.class;
        try {
            // 构造Cat实例
            Constructor constructor = clazz.getConstructor(String.class, int.class);
            Object cat = constructor.newInstance("jsai",23);

            // 调用无参方法
            Method sleep = clazz.getDeclaredMethod("sleep");
            sleep.invoke(cat);

            // 调用定项参数方法
            Method eat = clazz.getDeclaredMethod("eat", String.class);
            eat.invoke(cat, "testString");

            // 调用不定项参数方法,不定项参数可以当成数组来处理
            Method varargsEat = clazz.getDeclaredMethod("eat", String[].class);
            String[] foods = new String[]{"str1","str2","str3"};
            varargsEat.invoke(cat, (Object) foods);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

结果:
在这里插入图片描述

3、Constructor类

(1)获取构造方法
方法 解释
getConstructors() 获取所有的public构造方法
getDeclaredConstructors() 获取所有声明的构造方法
getConstructor(Class<?>... parameterTypes) 获取指定public构造函数
getDeclaredConstructor(Class<?>... parameterTypes) 获取指定构造函数

参数parameterTypes为构造方法的参数类型
构造方法的名称、限定符、参数、声明的异常等获取方法都与Method类似,请参照Method

(2)创建对象

通过反射有两种方法可以创建对象:

  1. java.lang.reflect.Constructor.newInstance()
  2. Class.newInstance()

一般来讲,我们优先使用第一种方法;那么这两种方法有何异同呢?

  • Class.newInstance()仅可用来调用无参的构造方法;Constructor.newInstance()可以调用任意参数的构造方法。
  • Class.newInstance()会将构造方法中抛出的异常不作处理原样抛出;Constructor.newInstance()会将构造方法中抛出的异常都包装成InvocationTargetException抛出。
  • Class.newInstance()需要拥有构造方法的访问权限;Constructor.newInstance()可以通过setAccessible(true)方法绕过访问权限访问private构造方法。

注意:反射不支持自动封箱,传入参数时要小心(自动封箱是在编译期间的,而反射在运行期间)

四、数组和枚举

数组和枚举也是对象,但是在反射中,对数组和枚举的创建、访问和普通对象有一些的不同,所以Java反射为数组和枚举提供了一些特定的API接口。

数组

(1)数组类型

数组类型:数组本质是一个对象,所以它也有自己的类型。
例如对于int[] intArray,数组类型为class [I。数组类型中的[个数代表数组的维度,例如[代表一维数组,[[代表二维数组;[后面的字母代表数组元素类型,I代表int,一般为类型的首字母大写(long类型例外,为J)。

class [B    //byte类型一维数组
class [S    //short类型一维数组
class [I    //int类型一维数组
class [C    //char类型一维数组
class [J    //long类型一维数组,J代表long类型,因为L被引用对象类型占用了
class [F    //float类型一维数组
class [D    //double类型一维数组
class [Lcom.dada.Season    //引用类型一维数组
class [[Ljava.lang.String  //引用类型二维数组
//获取一个变量的类型
Class<?> c = field.getType();
//判断该变量是否为数组
if (c.isArray()) {
   //获取数组的元素类型
   c.getComponentType()
}
(2)创建和初始化数组

Java反射为我们提供了java.lang.reflect.Array类用来创建和初始化数组。

//创建数组, 参数componentType为数组元素的类型,后面不定项参数的个数代表数组的维度,参数值为数组长度
Array.newInstance(Class<?> componentType, int... dimensions)

//设置数组值,array为数组对象,index为数组的下标,value为需要设置的值
Array.set(Object array, int index, int value)

//获取数组的值,array为数组对象,index为数组的下标
Array.get(Object array, int index)

例如:用反射创建 int[] array = new int[]{1, 2}

Object array = Array.newInstance(int.class, 2);
Array.setInt(array , 0, 1);
Array.setInt(array , 1, 2);

注意:反射支持对数据自动加宽,但不允许数据narrowing(变窄?真难翻译)。意思是对于上述set方法,你可以在int类型数组中 set short类型数据,但不可以set long类型数据,否则会报IllegalArgumentException。

(3)多维数组

Java反射没有提供能够直接访问多维数组元素的API,但你可以把多维数组当成数组的数组处理。

Object matrix = Array.newInstance(int.class, 2, 2);
Object row0 = Array.get(matrix, 0);
Object row1 = Array.get(matrix, 1);

Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
Object matrix = Array.newInstance(int.class, 2);
Object row0 = Array.newInstance(int.class, 2);
Object row1 = Array.newInstance(int.class, 2);

Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);

Array.set(matrix, 0, row0);
Array.set(matrix, 1, row1);

枚举

枚举隐式继承自java.lang.Enum,Enum继承自Object,所以枚举本质也是一个类,也可以有成员变量,构造方法,方法等;对于普通类所能使用的反射方法,枚举都能使用;另外java反射额外提供了几个方法为枚举服务。

方法 解释
Class.isEnum() 指示此类是否表示枚举类型
Class.getEnumConstants() 检索由枚举定义的枚举常量的列表,这些常量的声明顺序为
java.lang.reflect.Field.isEnumConstant() 指示此字段是否表示枚举类型的元素

五、反射的缺点

  1. 性能开销
    反射涉及类型动态解析,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  2. 安全限制
    使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。
  3. 内部曝光
    由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

使用反射的一个原则:如果使用常规方法能够实现,那么就不要用反射。

转载:https://www.jianshu.com/p/607ff4e79a13

发布了75 篇原创文章 · 获赞 39 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/s2152637/article/details/103685124