Java基础之反射机制

一、概述

1.1 基本概念

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。

Java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。Java反射机制主要提供了以下功能,这些功能都位于java.lang.reflect包。

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。
  • 生成动态代理。

要想知道一个类的属性和方法,必须先获取到该类的字节码文件对象。获取类的信息时,使用的就是Class类中的方法。所以先要获取到每一个字节码文件(.class)对应的Class类型的对象。

1.2 优缺点

优点:

  • 能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
  • Java动态编译相结合,可以实现无比强大的功能。
  • 对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:

  • 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
  • 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。

1.3 reflect包与核心类Class

实现java反射机制的类都位于java.lang.reflect包中,java.lang.Class类是Java反射机制API中的核心类。

java.lang.reflect包提供了反射中用到类,主要的类说明如下:

  • Constructor类:提供类的构造方法信息。
  • Field类:提供类或接口中成员变量信息。
  • Method类:提供类或接口成员方法信息。
  • Array类:提供了动态创建和访问Java数组的方法。
  • Modifier类:提供类和成员访问修饰符信息。

1.4 反射的原理

JAVA语言编译之后会生成一个.class文件,这些Class对象承载了这个类型的父类、接口、构造函数、方法、属性等原始信息,这些class文件在程序运行时会被ClassLoader加载到虚拟机中。

反射就是通过字节码文件找到某一个类、类中的方法以及属性等。

二、反射常用方法

利用反射机制能获得类的所有信息 ,类中有什么信息,它就可以获得什么信息。

反射的实现主要借助以下四个类

  • Class:类的对象
  • Constructor:类的构造方法
  • Field:类中的属性对象
  • Method:类中的方法对象

2.1 访问类

2.1.1 获取class实例

java.lang.Class类是实现反射的关键所在,Class类的一个实例表示Java的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和voidClass没有公有的构造方法,Class实例是由JVM在类加载时自动创建的。

获取class实例的三种方式:

  • Object类的getClass()方法: 类的对象.getClass()(创建类的对象),调用对象的getClass()方法获取Class对象
  • 静态属性class: 通过类名的class属性获取Class对象
  • Class类中的静态方法forName()Class.forName("类的路径"),先将类的字节码文件加载到内存,再返回Class对象

在程序代码中获得Class实例可以通过如下代码实现:

public class Test {
    public static void main(String[] args) {
        // 1. 类名.class
        Class clazz1 = String.class;
        // 2. 类的对象.getClass()
        String str2 = "Hello";
        Class clazz2 = str2.getClass();
        // 3. Class.forName("")
        Class clazz3 = Class.forName("java.lang.String");
    }
}

每一种类型包括类和接口等,都有一个class静态变量可以获得Class实例。另外,每一个对象都有getClass()方法可以获得Class实例,该方法是由Object类提供的实例方法。

2.1.2 获取class对象摘要信息

public class Test {
    public static void main(String[] args) {
        boolean isPrimitive = clazz1.isPrimitive();//判断是否是基础类型
        boolean isArray = clazz1.isArray();//判断是否是集合类
        boolean isAnnotation = clazz1.isAnnotation();//判断是否是注解类
        boolean isInterface = clazz1.isInterface();//判断是否是接口类
        boolean isEnum = clazz1.isEnum();//判断是否是枚举类
        boolean isAnonymousClass = clazz1.isAnonymousClass();//判断是否是匿名内部类
        boolean isAnnotationPresent = clazz1.isAnnotationPresent(Deprecated.class);//判断是否被某个注解类修饰

        String className = clazz1.getName();//获取class名字 包含包名路径
        Package aPackage = clazz1.getPackage();//获取class的包信息
        String simpleName = clazz1.getSimpleName();//获取class类名
        int modifiers = clazz1.getModifiers();//获取class访问权限

        Class<?>[] declaredClasses = clazz1.getDeclaredClasses();//内部类
        Class<?> declaringClass = clazz1.getDeclaringClass();//外部类
    }
}

2.2 访问构造方法

为了能够动态获取对象构造方法的信息,首先需要通过下列方法之一创建一个Constructor类型的对象或者数组。

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

创建的每个Constructor对象表示一个构造方法,然后利用Constructor对象的方法操作构造方法。

getConstructors()和getDeclaredConstructors()区别
getConstructors():获得某个类的所有的公共(public)的构造方法,包括父类中的构造方法。
getDeclaredConstructors():获得某个类的所有声明的构造方法,即包括public、private和proteced,但是不包括父类的申明构造方法。
同样类似的还有getMethods()和getDeclaredMethods()。

Constructor类的常用方法

方法名称 说明
isVarArgs() 查看该构造方法是否允许带可变数量的参数,如果允许,返回true,否则返回false
getParameterTypes() 按照声明顺序以Class数组的形式获取该构造方法各个参数的类型

通过java.lang.reflect.Modifier类可以解析出getMocMers()方法的返回值所表示的修饰符信息。在该类中提供了一系列用来解析的静态方法,既可以查看是否被指定的修饰符修饰,还可以字符串的形式获得所有修饰符。

2.2.1 Modifier类的常用静态方法

静态方法名称 说明
isStatic(int mod) 如果使用static修饰符修饰则返回true,否则返回false
isPublic(int mod) 如果使用public修饰符修饰则返回true,否则返回false
isProtected(int mod) 如果使用protected修饰符修饰则返回true,否则返回false
isPrivate(int mod) 如果使用private修饰符修饰则返回true,否则返回false
isFinal(int mod) 如果使用final修饰符修饰则返回true,否则返回false
toString(int mod) 以字符串形式返回所有修饰符

2.2.2 示例

public class Test {
    public static void main(String[] args) throws Exception {
        //获取class对象的所有声明构造函数
        Constructor<?>[] allConstructors = clazz1.getDeclaredConstructors();
        //获取class对象public构造函数
        Constructor<?>[] publicConstructors = clazz1.getConstructors();
        //获取指定声明构造函数
        Constructor<?> constructor = clazz1.getDeclaredConstructor(new Class[]{String.class});
        //获取指定声明的public构造函数
        Constructor publicConstructor = clazz1.getConstructor(new Class[]{});

        Class[] parameterTypes = constructor.getParameterTypes();
        //利用Constructor对象实例化一个类
        Constructor constructor = clazz1.getConstructor(String.class);
        MyObject myObject = (MyObject)constructor.newInstance("constructor-arg1");
    }
}

返回的Constructor数组包含每一个声明为公有的(Public)构造方法。如果知道要访问的构造方法的方法参数类型,可以用下面的方法获取指定的构造方法,这例子返回的构造方法的方法参数为String类型:

constructor.newInstance()方法的方法参数是一个可变参数列表,但是当调用构造方法的时候必须提供精确的参数,即形参与实参必须一一对应。在这个例子中构造方法需要一个String类型的参数,那我们在调用newInstance方法的时候就必须传入一个String类型的参数。

2.3 访问方法(获取方法)

动态获取一个对象方法的信息,首先需要通过下列方法之一创建一个Method类型的对象或者数组。

  • getMethods()
  • getMethods(String name, Class<?>… parameterTypes)
  • getDeclaredMethods()
  • getDeclaredMethods(String name, Class<?>... parameterTypes)

2.3.1 Method类的常用方法

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

2.3.2 示例

public class Test {
    public static void main(String[] args) throws Exception {
        //获取class对象的所有声明方法
        Method[] methods = clazz1.getDeclaredMethods();
        //获取class对象的所有方法 包括父类的方法
        Method[] allMethods = clazz1.getMethods();
        //通过参数类型来获取指定的方法
        Method method = clazz1.getMethod("doSomething", new Class[]{String.class});
        //获取的方法没有参数,那么在调用`getMethod()`方法时第二个参数传入`null`即可
        Method method = clazz1.getMethod("doSomething", null);
        //获取指定方法的方法参数
        Class[] parameterTypes = method.getParameterTypes();
        //获取指定方法的返回类型
        Class returnType = method.getReturnType();

        //获取一个方法名为doSomething,参数类型为String的方法
        Method method = clazz1.getMethod("doSomething", String.class);
        Object returnValue = method.invoke(null, "parameter-value1");
    }
}

传入的null参数是要调用方法的对象,如果是一个静态方法调用的话则可以用null代替指定对象作为invoke()的参数,在上面这个例子中,如果doSomething不是静态方法的话,就要传入有效的MyObject实例而不是null

Method.invoke(Object target, Object... parameters)方法的第二个参数是一个可变参数列表,但是必须要传入与要调用方法的形参一一对应的实参。就像上个例子那样,方法需要String类型的参数,那我们必须要传入一个字符串。

2.4 访问成员变量

通过下列任意一个方法访问成员变量时将返回Field类型的对象或数组。

  • getFields()
  • getField(String name)
  • getDeclaredFields()
  • getDeclaredField(String name)

2.4.1 Field类的常用方法

方法名称 说明
getName() 获得该成员变量的名称
getType() 获取表示该成员变量的Class对象
get(Object obj) 获得指定对象obj中成员变量的值,返回值为Object类型
set(Object obj, Object value) 将指定对象obj中成员变量的值设置为value
getInt(Object obj) 获得指定对象obj中成员类型为int的成员变量的值
setInt(Object 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() 获得可以解析出该方法所采用修饰符的整数

2.4.2 示例

public class Test {
    public static void main(String[] args) throws Exception {
        //获取class对象的所有属性
        Field[] allFields = clazz1.getDeclaredFields();
        //获取class对象的public属性
        Field[] publicFields = clazz1.getFields();
        //获取class指定属性
        Field ageField = clazz1.getDeclaredField("age");
        //获取class指定的public属性
        Field desField = clazz1.getField("des");
        //获取属性名称
        String fieldName = desField.getName();
        //获取属性名称
        Object fieldType = desField.getType();
        //获取或设置(get/set)变量值
        Class  aClass = MyObject.class;
        Field field = aClass.getField("someField");
        MyObject objectInstance = new MyObject();
        Object value = field.get(objectInstance);
        field.set(objetInstance, value);
    }
}

传入Field.get()/Field.set()方法的参数objetInstance应该是拥有指定变量的类的实例。在上述的例子中传入的参数是MyObject类的实例,是因为someFieldMyObject类的实例。

如果变量是静态变量的话(public static)那么在调用Field.get()/Field.set()方法的时候传入null做为参数而不用传递拥有该变量的类的实例。(译者注:如果传入拥有该变量的类的实例也可以得到相同的结果)

三、深入探索

3.1 注解

通过下列任意一个方法访问注解时将返回Annotation类型的对象或数组。

  • getAnnotations()
  • getAnnotation(Class annotationClass)
  • getDeclaredAnnotations()
  • getDeclaredAnnotation(Class annotationClass)

注解是插入代码中的一种注释或者说是一种元数据(meta data)。这些注解信息可以在编译期使用预编译工具进行处理(pre-compiler tools),也可以在运行期使用Java反射机制进行处理。下面是一个类注解的例子:

@MyAnnotation(name="someName",  value = "Hello World")
public class TheClass {
}

TheClass类定义的上面有一个@MyAnnotation的注解。注解的定义与接口的定义相似,下面是MyAnnotation注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    public String name();
    public String value();
}

在interface前面的@符号表名这是一个注解,一旦定义了一个注解之后就可以将其应用到代码中。

在注解定义中的两个指示@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE),说明了这个注解该如何使用。

  • @Retention(RetentionPolicy.RUNTIME)表示这个注解可以在运行期通过反射访问。如果没有在注解定义的时候使用这个指示那么这个注解的信息不会保留到运行期,这样反射就无法获取它的信息。
  • @Target(ElementType.TYPE)表示这个注解只能用在类型上面(比如类跟接口)。同样可以把Type改为Field或者Method,或者可以不用这个指示,这样的话注解在类,方法和变量上就都可以使用了。

3.1.1 类注解

在运行期访问类,方法或者变量的注解信息,下是一个访问类注解的例子:

public class Test {
    public static void main(String[] args) {
        //在运行期访问类,方法或者变量的注解信息
        Class aClass = TheClass.class;
        Annotation[] annotations = aClass.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof MyAnnotation) {
                MyAnnotation myAnnotation = (MyAnnotation) annotation;
                System.out.println("name: " + myAnnotation.name());
                System.out.println("value: " + myAnnotation.value());
            }
        }
        //访问一个类的注解
        Annotation annotation = aClass.getAnnotation(MyAnnotation.class);
        if (annotation instanceof MyAnnotation) {
            MyAnnotation myAnnotation = (MyAnnotation) annotation;
            System.out.println("name: " + myAnnotation.name());
            System.out.println("value: " + myAnnotation.value());
        }
    }
}

3.1.2 方法注解

访问方法注解:

public class Test {
    public static void main(String[] args) {
        Method method = clazz1.getMethod("doSomething", null);
        Annotation[] annotations = method.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof MyAnnotation) {
                MyAnnotation myAnnotation = (MyAnnotation) annotation;
                System.out.println("name: " + myAnnotation.name());
                System.out.println("value: " + myAnnotation.value());
            }
        }
        //访问指定的方法注解
        Annotation annotation = method.getAnnotation(MyAnnotation.class);
        if (annotation instanceof MyAnnotation) {
            MyAnnotation myAnnotation = (MyAnnotation) annotation;
            System.out.println("name: " + myAnnotation.name());
            System.out.println("value: " + myAnnotation.value());
        }
    }
    class TheClass {
        @MyAnnotation(name="someName",  value = "Hello World")
        public void doSomething(){}
    }
}

3.1.3 参数注解

可以通过Method对象来访问方法参数注解:

public class Test {
    public static void main(String[] args) {
        Method method = clazz1.getMethod("doSomething", null);
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Class[] parameterTypes = method.getParameterTypes();
        int i = 0;
        for (Annotation[] annotations : parameterAnnotations) {
            Class parameterType = parameterTypes[i++];
            for (Annotation annotation : annotations) {
                if (annotation instanceof MyAnnotation) {
                    MyAnnotation myAnnotation = (MyAnnotation) annotation;
                    System.out.println("param: " + parameterType.getName());
                    System.out.println("name : " + myAnnotation.name());
                    System.out.println("value: " + myAnnotation.value());
                }
            }
        }
    }
    class TheClass {
        public static void doSomethingElse(@MyAnnotation(name="aName", value="aValue") String parameter) {
        }
    }
}

需要注意的是Method.getParameterAnnotations()方法返回一个注解类型的二维数组,每一个方法的参数包含一个注解数组。

3.1.4 变量注解

下面是一个变量注解的例子:

public class Test {
    public static void main(String[] args) {
        Field field = clazz1.getField("des");
        Annotation[] annotations = field.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof MyAnnotation) {
                MyAnnotation myAnnotation = (MyAnnotation) annotation;
                System.out.println("name: " + myAnnotation.name());
                System.out.println("value: " + myAnnotation.value());
            }
        }
        //访问指定的变量注解
        Annotation annotation = field.getAnnotation(MyAnnotation.class);
        if (annotation instanceof MyAnnotation) {
            MyAnnotation myAnnotation = (MyAnnotation) annotation;
            System.out.println("name: " + myAnnotation.name());
            System.out.println("value: " + myAnnotation.value());
        }
    }
    class TheClass {
        @MyAnnotation(name="someName",  value = "Hello World")
        public String myField = null;
    }
}

3.2 泛型

通过下列任意一个方法访问泛型时将返回Type类型的对象或数组。

  • getGenericType()
  • getGenericReturnType()
  • getGenericParameterTypes()

3.2.1 运用泛型反射的经验法则

下面是两个典型的使用泛型的场景:

  1. 声明一个需要被参数化(parameterizable)的类/接口。
  2. 使用一个参数化类。

当声明一个类或者接口的时候可以指明这个类或接口可以被参数化,java.util.List接口就是典型的例子。你可以运用泛型机制创建一个标明存储的是String类型list,这样比你创建一个Objectlist要更好。

当你想在运行期参数化类型本身,比如你想检查java.util.List类的参数化类型,你是没有办法能知道他具体的参数化类型是什么。这样一来这个类型就可以是一个应用中所有的类型。但是,当你检查一个使用了被参数化的类型的变量或者方法,你可以获得这个被参数化类型的具体参数。

总之:不能在运行期获知一个被参数化的类型的具体参数类型是什么,但是可以在用到这个被参数化类型的方法以及变量中找到他们,换句话说就是获知他们具体的参数化类型。

3.2.2 泛型方法返回类型

如果获得了java.lang.reflect.Method对象,那么就可以获取到这个方法的泛型返回类型信息。如果方法是在一个被参数化类型之中(译者注:如T fun())那么无法获取他的具体类型,但是如果方法返回一个泛型类(译者注:如List fun())那么就可以获得这个泛型类的具体参数化类型。

我们可以获取getStringList()方法的泛型返回类型,换句话说,我们可以检测到getStringList()方法返回的是List而不仅仅只是一个List。如下例:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MyClass.class.getMethod("getStringList", null);
        Type returnType = method.getGenericReturnType();
        if (returnType instanceof ParameterizedType) {
            ParameterizedType type = (ParameterizedType) returnType;
            Type[] typeArguments = type.getActualTypeArguments();
            for (Type typeArgument : typeArguments) {
                Class typeArgClass = (Class) typeArgument;
                System.out.println("typeArgClass = " + typeArgClass);
            }
        }
    }
    class MyClass {
        protected List<String> stringList = new ArrayList<>();
        public List<String> getStringList() {
            return this.stringList;
        }
    }
}

这段代码会打印出typeArgClass = class java.lang.String

3.2.3 泛型方法参数类型

可以通过反射来获取方法参数的泛型类型,下面这个例子定义了一个类,这个类中的方法的参数是一个被参数化的List

public class Test {
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MyClass.class.getMethod("setStringList", List.class);
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        for (Type genericParameterType : genericParameterTypes) {
            if (genericParameterType instanceof ParameterizedType) {
                ParameterizedType aType = (ParameterizedType) genericParameterType;
                Type[] parameterArgTypes = aType.getActualTypeArguments();
                for (Type parameterArgType : parameterArgTypes) {
                    Class parameterArgClass = (Class) parameterArgType;
                    System.out.println("parameterArgClass = " + parameterArgClass);
                }
            }
        }
    }
    class MyClass {
        protected List<String> stringList = new ArrayList<>();
        public void setStringList(List<String> list){
            this.stringList = list;
        }
    }
}

这段代码会打印出parameterArgType = class java.lang.String

3.2.4 泛型变量类型

同样可以通过反射来访问公有(Public)变量的泛型类型,无论这个变量是一个类的静态成员变量或是实例成员变量。你可以在“Java Reflection:Fields”中阅读到有关如何获取Field对象的相关内容。这是之前的一个例子,一个定义了一个名为stringList的成员变量的类。

public class Test {
    public static void main(String[] args) throws NoSuchFieldException {
        Field field = MyClass.class.getField("stringList");
        Type genericFieldType = field.getGenericType();
        if (genericFieldType instanceof ParameterizedType) {
            ParameterizedType aType = (ParameterizedType) genericFieldType;
            Type[] fieldArgTypes = aType.getActualTypeArguments();
            for (Type fieldArgType : fieldArgTypes) {
                Class fieldArgClass = (Class) fieldArgType;
                System.out.println("fieldArgClass = " + fieldArgClass);
            }
        }
    }
    class MyClass {
        public List<String> stringList = new ArrayList<>();
    }
}

这段代码会打印出fieldArgClass = class java.lang.String

3.3 数组

Java反射机制通过java.lang.reflect.Array这个类来处理数组。不要把这个类与Java集合套件(Collections suite)中的java.util.Arrays混淆,java.util.Arrays是一个提供了遍历数组,将数组转化为集合等工具方法的类。

3.3.1 创建数组

Java反射机制通过java.lang.reflect.Array类来创建数组。下面是一个如何创建数组的例子:

public class Test {
    public static void main(String[] args) {
        int[] intArray = (int[]) Array.newInstance(int.class, 3);
    }
}

这个例子创建一个int类型的数组。Array.newInstance()方法的第一个参数表示了我们要创建一个什么类型的数组。第二个参数表示了这个数组的空间是多大。

3.3.2 访问数组

通过Java反射机制同样可以访问数组中的元素。具体可以使用Array.get(…)Array.set(…)方法来访问。下面是一个例子:

public class Test {
    public static void main(String[] args) {
        int[] intArray = (int[]) Array.newInstance(int.class, 3);
        Array.set(intArray, 0, 123);
        Array.set(intArray, 1, 456);
        Array.set(intArray, 2, 789);
        System.out.println("intArray[0] = " + Array.get(intArray, 0));
        System.out.println("intArray[1] = " + Array.get(intArray, 1));
        System.out.println("intArray[2] = " + Array.get(intArray, 2));
    }
}

这个例子会输出:

intArray[0] = 123
intArray[1] = 456
intArray[2] = 789

3.3.3 获取数组的Class对象

public class Test {
    public static void main(String[] args) throws Exception {
        //获得一个原生数据类型(primitive)int数组的Class对象
        Class integerArrayClass = Class.forName("[I");

        //获取数组的Class对象(不通过反射)
        Class stringArrayClass1 = String[].class;
        //获取普通对象类型的数组
        Class stringArrayClass2 = Class.forName("[Ljava.lang.String;");
        //指定的类型创建一个空的数组,然后通过这个空的数组来获取数组的Class对象
        Class theClass = getClass("int");
        Class stringArrayClass3 = Array.newInstance(theClass, 0).getClass();
        //校验Class对象是不是代表一个数组
        Class stringArrayClass4 = Array.newInstance(String.class, 0).getClass();
        System.out.println("stringArrayClass3 is array: " + stringArrayClass4.isArray());
    }

    //获取普通对象以及原生对象的Class对象
    public static Class getClass(String className) throws ClassNotFoundException {
        if ("int".equals(className)) return int.class;
        if ("long".equals(className)) return long.class;
        //...
        return Class.forName(className);
    }
}

JVM中字母I代表int类型,左边的‘[’代表我想要的是一个int类型的数组,这个规则同样适用于其他的原生数据类型。

JVM指令集中,大多数的指令都包含了其所操作的数据类型信息。数据类型相关的操作码助记符中的首字母都跟操作的数据类型相关:

指令 数据类型
i int
l long
s short
b byte
c char
f float
d double
a reference

注意‘[L’的右边是类名,类名的右边是一个‘;’符号。这个的含义是一个指定类型的数组。

3.3.4 获取数组的成员类型

一旦你获取了一个数组的Class对象,你就可以通过Class.getComponentType()方法获取这个数组的成员类型。成员类型就是数组存储的数据类型。例如数组int[]的成员类型就是一个Class对象int.classString[]的成员类型就是java.lang.String类的Class对象。

下面是一个访问数组成员类型的例子:

public class Test {
    public static void main(String[] args) {
        String[] strings = new String[3];
        Class stringArrayClass = strings.getClass();
        Class stringArrayComponentType = stringArrayClass.getComponentType();
        System.out.println(stringArrayComponentType);
    }
}

四、高级应用

反射机制不仅在基础开发中有着广泛的应用,还在高级场景中发挥着重要作用,例如动态代理、框架设计、设计模式等。以下是对这些高级应用的详细解析。

4.1 动态代理与反射

动态代理是反射机制的经典应用之一,它允许在运行时动态生成代理类,从而在不修改原始类的情况下,为对象添加额外的功能。

4.1.1 动态代理的原理

动态代理的核心是java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。Proxy类用于生成代理对象,而InvocationHandler接口定义了代理对象的行为。

动态代理的工作流程如下:

  • 定义接口:代理类和目标类需要实现相同的接口。
  • 实现InvocationHandler接口:在invoke方法中定义代理类的行为。
  • 使用Proxy.newProxyInstance生成代理对象:通过反射动态生成代理对象。

4.1.2 使用反射实现动态代理

以下是一个动态代理的完整示例:

定义接口

public interface Calculator {
    int add(int a, int b);
}

实现目标类

public class SimpleCalculator implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

实现InvocationHandler接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class CalculatorProxy implements InvocationHandler {
    private Object target; // 目标对象

    public CalculatorProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 记录方法调用前的日志
        System.out.println("Calling method: " + method.getName() + " with args: " + args[0] + ", " + args[1]);
        // 调用目标方法
        Object result = method.invoke(target, args);
        // 记录方法调用后的日志
        System.out.println("Method result: " + result);
        return result;
    }
}

生成代理对象

import java.lang.reflect.Proxy;

public class DynamicProxyExample {
    public static void main(String[] args) {
        // 创建目标对象
        Calculator target = new SimpleCalculator();
        // 创建代理对象
        Calculator proxy = (Calculator) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 类加载器
            target.getClass().getInterfaces(),  // 目标对象的接口
            new CalculatorProxy(target)         // 代理处理器
        );
        // 调用代理对象的方法
        proxy.add(10, 20);
    }
}

输出:

Calling method: add with args: 10, 20
Method result: 30

在这个例子中,通过反射生成了一个代理对象,并在调用目标方法前后添加了日志功能。

4.2 反射在框架中的应用

反射机制是许多框架(如Spring、Hibernate)的核心技术之一,它使得框架能够在运行时动态加载和配置组件。

4.2.1 Spring框架中的反射应用

Spring框架的核心功能之一是依赖注入(Dependency InjectionDI)。通过反射,Spring可以在运行时动态创建对象,并将依赖关系注入到对象中。

示例:Spring中的依赖注入

假设我们有一个UserService类,它依赖于UserRepository

public class UserService {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(String name) {
        userRepository.save(name);
    }
}

Spring配置文件中,我们可以通过反射将UserRepository注入到UserService中:

<bean id="userRepository" class="com.example.UserRepository" />
<bean id="userService" class="com.example.UserService">
    <property name="userRepository" ref="userRepository" />
</bean>

Spring通过反射读取配置文件,动态创建UserServiceUserRepository的实例,并将UserRepository注入到UserService中。

4.2.2 Java EE中的反射应用

Java EE中,反射常用于处理注解和动态加载组件。例如Servlet容器通过反射加载和初始化Servlet类。

示例:Servlet中的反射应用

假设我们有一个自定义Servlet类:

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Hello from MyServlet");
    }
}

在web.xml中配置Servlet:

<servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>com.example.MyServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>MyServlet</servlet-name>
    <url-pattern>/myServlet</url-pattern>
</servlet-mapping>

Servlet容器通过反射加载MyServlet类,并调用其doGet方法处理HTTP请求。

4.3 反射与设计模式

反射机制可以与设计模式结合,实现更灵活和可扩展的代码结构。

4.3.1 反射在工厂模式中的应用

工厂模式是一种创建型设计模式,用于封装对象的创建过程。通过反射,可以动态地创建对象,从而增强工厂模式的灵活性。

示例:反射实现工厂模式
假设我们有一个接口Shape和多个实现类:

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Rectangle");
    }
}

通过反射实现工厂类:

public class ShapeFactory {
    public static Shape createShape(String className) throws Exception {
        Class<?> clazz = Class.forName(className);
        return (Shape) clazz.getDeclaredConstructor().newInstance();
    }
}

使用工厂类创建对象:

public class FactoryExample {
    public static void main(String[] args) throws Exception {
        Shape circle = ShapeFactory.createShape("com.example.Circle");
        circle.draw(); // 输出:Drawing Circle

        Shape rectangle = ShapeFactory.createShape("com.example.Rectangle");
        rectangle.draw(); // 输出:Drawing Rectangle
    }
}

在这个例子中,通过反射动态创建了CircleRectangle对象。

4.3.2 反射在策略模式中的应用

策略模式是一种行为型设计模式,用于定义一系列算法,并将它们封装在独立的类中。通过反射,可以动态选择和执行策略。

示例:反射实现策略模式

假设我们有一个接口Strategy和多个实现类:

public interface Strategy {
    void execute();
}

public class StrategyA implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing Strategy A");
    }
}

public class StrategyB implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing Strategy B");
    }
}

通过反射动态选择策略:

public class StrategyExample {
    public static void main(String[] args) throws Exception {
        String strategyName = "com.example.StrategyA"; // 可以从配置文件中读取
        Class<?> clazz = Class.forName(strategyName);
        Strategy strategy = (Strategy) clazz.getDeclaredConstructor().newInstance();
        strategy.execute(); // 输出:Executing Strategy A
    }
}

在这个例子中,通过反射动态选择了StrategyA并执行。

五、性能优化与最佳实践

反射机制虽然功能强大,但其性能开销较大,尤其是在频繁调用的情况下。为了充分发挥反射的优势,同时避免其性能问题,开发者需要了解反射的性能开销原因,掌握优化技巧,并遵循最佳实践。

5.1 性能问题

5.1.1 性能开销的原因

反射的性能开销主要来自以下几个方面:

  1. 动态解析:反射操作(如获取字段、方法、构造函数)需要在运行时动态解析类的元数据,这比直接调用代码要慢。例如Method.invoke()方法需要动态解析方法的调用,而直接调用方法则不需要。
  2. 安全性检查:反射操作会进行额外的安全性检查,例如访问控制检查(如setAccessible(true))。这些检查会增加额外的开销。
  3. 缓存缺失:如果每次反射操作都重新获取Class对象或成员信息,会导致频繁的JVM内部缓存缺失,从而降低性能。
  4. 类型转换和装箱/拆箱:反射操作通常涉及类型转换和装箱/拆箱操作,这些操作也会增加性能开销。

5.1.2 如何衡量反射的性能影响

衡量反射性能影响的方法包括:

  • 基准测试:使用工具(如JMH)对反射操作和直接调用进行基准测试,比较它们的性能差异。例如测试Method.invoke()和直接调用方法的性能。
  • 性能分析工具:使用性能分析工具(如JProfilerVisualVM)分析反射操作的性能瓶颈。例如分析反射操作的CPU使用率和内存占用。
  • 代码覆盖率分析:分析反射操作的调用频率,判断是否存在过度使用反射的情况。

5.2 优化技巧

5.2.1 缓存反射对象

缓存反射对象是优化反射性能的关键技巧之一。通过缓存Class对象、Method对象、Field对象等,可以避免重复的动态解析和安全性检查。

示例:缓存Method对象

import java.lang.reflect.Method;

public class ReflectionCacheExample {
    private static Method cachedMethod;
    static {
        try {
            cachedMethod = String.class.getMethod("length");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception {
        String str = "Hello, Java";
        // 使用缓存的Method对象
        int length = (int) cachedMethod.invoke(str);
        System.out.println("Length: " + length); // 输出:Length: 11
    }
}

在这个例子中,length方法被缓存,避免了每次调用时的动态解析。

5.2.2 使用反射注意事项

  • 避免频繁调用反射:如果反射操作被频繁调用,建议将其封装为工具类,并缓存反射对象。
  • 优先使用直接调用:在性能敏感的场景中,优先使用直接调用,而不是反射。
  • 减少安全性检查:如果需要频繁访问私有成员,可以提前调用setAccessible(true),减少每次调用的安全性检查开销。
  • 避免不必要的反射操作:例如避免在循环中重复获取Class对象或成员信息。

5.3 最佳实践

5.3.1 应用场景

反射适用于以下场景:

  • 动态加载类:在编译时无法确定类名的情况下,反射可以动态加载类。例如根据配置文件或用户输入动态加载不同的实现类。
  • 框架设计:反射是许多框架(如SpringHibernate)的核心技术,用于实现依赖注入、AOP等功能。
  • 插件化系统:反射可以用于实现插件化系统,动态加载和调用插件。
  • 单元测试:反射可以用于访问私有成员,方便单元测试。

5.3.2 不适场景

反射不适用于以下场景:

  • 性能敏感的场景:在性能要求较高的场景中,反射的性能开销可能无法接受。例如高频交易系统或实时系统。
  • 代码可读性要求高的场景:反射代码通常比直接调用代码更复杂,降低了代码的可读性和维护性。
  • 安全性要求高的场景:反射可以突破访问控制限制,可能导致代码的安全性问题。例如在多线程环境中使用反射访问私有成员可能导致数据竞争。
  • 编译时已知类的场景:如果类名和成员信息在编译时已知,建议直接调用,而不是使用反射。

5.4 性能优化实际案例

5.4.1 缓存反射对象的实际应用

Spring框架中,反射被广泛用于依赖注入。为了优化性能,Spring缓存了大量的反射对象,例如BeanDefinitionMethod对象。

示例:Spring中的反射缓存

Spring通过CachingMetadataReaderFactory缓存类的元数据,避免重复解析类的信息。

public class  Test {
    public static void main(String[] args) {
        // Spring中的反射缓存示例
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean");
        Method method = beanDefinition.getFactoryMethod();
        method.setAccessible(true); // 设置访问权限
    }
}

5.4.2 反射与直接调用的性能对比

以下是一个简单的性能对比示例,比较直接调用和反射调用的性能差异:

import java.lang.reflect.Method;

public class PerformanceComparison {
    public static void main(String[] args) throws Exception {
        // 直接调用
        long startTime = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            "Hello, Java".length();
        }
        long endTime = System.nanoTime();
        System.out.println("Direct Call Time: " + (endTime - startTime) + " ns");

        // 反射调用
        Method method = String.class.getMethod("length");
        startTime = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            method.invoke("Hello, Java");
        }
        endTime = System.nanoTime();
        System.out.println("Reflection Call Time: " + (endTime - startTime) + " ns");
    }
}

输出:

Direct Call Time: 123456 ns
Reflection Call Time: 1234567 ns

在这个例子中,反射调用的性能明显低于直接调用。

六、实际案例分析

反射机制在实际开发中有着广泛的应用,尤其是在需要动态性和灵活性的场景中。以下是反射机制在插件机制、对象序列化和单元测试中的实际案例分析。

6.1 实现插件机制

插件机制是一种常见的扩展系统功能的方式,通过反射可以实现动态加载和调用插件。

6.1.1 插件机制的设计思路

插件机制的设计思路如下:

  1. 定义插件接口:所有插件都需要实现一个统一的接口,以便系统能够识别和调用插件。
  2. 动态加载插件:通过反射动态加载插件类,并创建插件实例。
  3. 调用插件功能:通过反射调用插件的方法,实现插件的功能。

6.1.2 使用反射动态加载插件

以下是一个完整的插件机制实现示例:

定义插件接口

public interface Plugin {
    void execute();
}

实现插件类

public class HelloPlugin implements Plugin {
    @Override
    public void execute() {
        System.out.println("Hello from HelloPlugin!");
    }
}

public class GoodbyePlugin implements Plugin {
    @Override
    public void execute() {
        System.out.println("Goodbye from GoodbyePlugin!");
    }
}

插件加载器

import java.io.FileInputStream;
import java.util.Properties;

public class PluginLoader {
    public static void main(String[] args) throws Exception {
        // 读取配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("plugins.properties"));
        // 获取插件类名
        String pluginClassName = properties.getProperty("pluginClass");
        // 通过反射加载插件类
        Class<?> pluginClass = Class.forName(pluginClassName);
        // 创建插件实例
        Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
        // 调用插件方法
        plugin.execute();
    }
}

配置文件 plugins.properties

pluginClass=com.example.HelloPlugin

运行结果

Hello from HelloPlugin!

在这个例子中,通过反射动态加载并调用了插件类,实现了插件机制的灵活性。

6.2 实现对象序列化

对象序列化是将对象转换为字节流的过程,反序列化则是将字节流转换为对象的过程。通过反射可以实现自定义的序列化和反序列化逻辑。

6.2.1 对象序列化的原理

对象序列化的原理如下:

  • 序列化:将对象的字段值转换为字节流,并保存到文件或网络中。
  • 反序列化:从字节流中读取数据,并恢复为对象的字段值。

6.2.2 使用反射处理序列化过程

以下是一个自定义序列化和反序列化的实现示例:

定义需要序列化的类

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Date;

@Data
@AllArgsConstructor
public class Person {
    private String name;
    private int age;
    private Date birthDate;
}

自定义序列化工具类

import java.io.*;
import java.lang.reflect.Field;
import java.util.Date;

public class CustomSerializer {
    public static void serialize(Object obj, String fileName) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            Class<?> clazz = obj.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true); // 设置访问权限
                oos.writeObject(field.get(obj)); // 写入字段值
            }
        }
    }

    public static <T> T deserialize(String fileName, Class<T> clazz) throws Exception {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            T obj = clazz.getDeclaredConstructor().newInstance();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true); // 设置访问权限
                field.set(obj, ois.readObject()); // 读取字段值
            }
            return obj;
        }
    }
}

测试序列化和反序列化

import java.util.Date;

public class SerializationExample {
    public static void main(String[] args) throws Exception {
        Person person = new Person("Alice", 30, new Date());
        // 序列化
        CustomSerializer.serialize(person, "person.ser");
        // 反序列化
        Person deserializedPerson = CustomSerializer.deserialize("person.ser", Person.class);
        System.out.println("Name: " + deserializedPerson.getName());
        System.out.println("Age: " + deserializedPerson.getAge());
        System.out.println("Birth Date: " + deserializedPerson.getBirthDate());
    }
}

运行结果

Name: Alice
Age: 30
Birth Date: Wed Oct 04 12:34:56 CST 2023

在这个例子中,通过反射实现了自定义的序列化和反序列化逻辑,展示了反射在对象序列化中的应用。

6.3 实现单元测试

单元测试是软件开发中的重要环节,反射可以用于访问私有成员和方法,从而方便单元测试的编写。

6.3.1 单元测试框架中的反射应用

许多单元测试框架(如JUnit)使用反射来执行测试方法。例如JUnit通过反射调用带有@Test注解的方法。

示例:JUnit中的反射应用

import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {
    @Test
    public void testAdd() {
        Calculator calculator = new Calculator();
        assertEquals(30, calculator.add(10, 20));
    }
}

在这个例子中,JUnit通过反射调用了testAdd方法,并验证了其结果。

6.3.2 自定义反射测试工具

通过反射,可以实现自定义的测试工具,用于访问私有成员和方法。

示例:自定义反射测试工具

import java.lang.reflect.Method;

public class ReflectionTestTool {
    public static void invokePrivateMethod(Object obj, String methodName,
                                           Object... args) throws Exception {
        Class<?> clazz = obj.getClass();
        Method method = clazz.getDeclaredMethod(methodName);
        method.setAccessible(true); // 设置访问权限
        method.invoke(obj, args); // 调用方法
    }
}

测试私有方法

public class PrivateMethodExample {
    private void privateMethod() {
        System.out.println("Private method called");
    }
    public static void main(String[] args) throws Exception {
        PrivateMethodExample obj = new PrivateMethodExample();
        ReflectionTestTool.invokePrivateMethod(obj, "privateMethod");
    }
}

运行结果

Private method called

在这个例子中,通过反射调用了私有方法,展示了反射在单元测试中的应用。

七、未来与发展

反射机制作为Java的核心特性之一,在过去的版本中不断演进,并在新兴技术中发挥着重要作用。同时,随着技术的发展,反射机制也面临着替代方案的挑战。以下是对反射机制未来发展的详细分析。

7.1 Java反射机制的演进

7.1.1 Java各版本中反射机制的改进

Java反射机制在各个版本中不断优化和改进,以提高性能和功能。

  • Java 5(J2SE 5.0):引入了泛型(Generics),反射机制增加了对泛型类型的支持。例如MethodField类新增了getGenericReturnType()getGenericParameterTypes()方法,用于获取泛型信息。
  • Java 7:引入了MethodHandleVarHandle,提供了更高效的反射调用方式。MethodHandle允许直接调用方法,避免了反射的性能开销。
  • Java 8:引入了Lambda表达式和方法引用,反射机制增加了对这些新特性的支持。例如Method类新增了isDefault()方法,用于判断方法是否为默认方法。
  • Java 9:引入了模块化系统(Jigsaw),反射机制增加了对模块的支持。例如Class类新增了getModule()方法,用于获取类的模块信息。
  • Java 11:引入了var关键字,反射机制增加了对局部变量类型的支持。例如Method类新增了getAnnotatedReturnType()方法,用于获取方法的注解类型。
  • Java 17(LTS):引入了密封类(Sealed Classes),反射机制增加了对密封类的支持。例如Class类新增了isSealed()方法,用于判断类是否为密封类。

7.1.2 反射机制的新特性

随着Java版本的演进,反射机制不断引入新特性,以满足现代开发的需求。

  • 泛型支持:反射机制提供了对泛型类型的支持,例如ParameterizedTypeTypeVariable。这些特性使得反射能够处理复杂的泛型类型。
  • 模块化支持:Java 9引入了模块化系统,反射机制增加了对模块的支持。例如Module类提供了addExports()方法,用于动态导出模块。
  • 性能优化:Java 7引入了MethodHandle,提供了更高效的反射调用方式。Java 11优化了反射的内部实现,进一步提高了性能。
  • 安全性增强:Java 9引入了模块化系统,增强了反射的安全性。例如反射操作需要显式声明模块的导出权限。

7.2 反射机制在新兴技术中的应用

反射机制在新兴技术中发挥着重要作用,例如微服务架构和云计算平台。

7.2.1 反射在微服务架构中的应用

微服务架构是一种分布式系统架构,反射机制在微服务中用于动态加载和调用服务。

  • 动态服务发现:反射可以用于动态加载和调用微服务,实现服务的动态发现和调用。例如Spring Cloud通过反射加载和调用服务实例。
  • 服务治理:反射可以用于实现服务治理功能,例如负载均衡和熔断。例如Spring Cloud Gateway通过反射调用服务实例。
  • 插件化扩展:反射可以用于实现插件化扩展,动态加载和调用插件。例如微服务框架可以通过反射加载和调用插件。

7.2.2 反射在云计算平台中的应用

云计算平台是一种分布式计算平台,反射机制在云计算中用于动态加载和调用组件。

  • 动态加载组件:反射可以用于动态加载和调用云计算平台的组件。例如AWS Lambda通过反射加载和调用用户定义的函数。
  • 服务编排:反射可以用于实现服务编排功能,例如动态调用服务。例如Kubernetes通过反射调用容器中的服务。
  • 插件化扩展:反射可以用于实现插件化扩展,动态加载和调用插件。例如云计算平台可以通过反射加载和调用插件。

7.3 反射机制的替代方案与发展趋势

随着技术的发展,反射机制面临着替代方案的挑战,例如代理机制和字节码操作框架。

7.3.1 代理机制的发展

代理机制是一种高效的反射替代方案,例如MethodHandleVarHandle

  • MethodHandle:Java 7引入的一种高效的反射调用方式。它允许直接调用方法,避免了反射的性能开销。
  • VarHandle:Java 9引入的一种高效的反射调用方式。它允许直接访问字段,避免了反射的性能开销。

7.3.2 字节码操作框架的兴起

字节码操作框架是一种高效的反射替代方案,例如ASMByteBuddy

  • ASM:一个轻量级的字节码操作框架,用于动态生成和修改字节码。它比反射更高效,适用于性能要求较高的场景。
  • ByteBuddy:一个高级的字节码操作框架,用于动态生成和修改字节码。它提供了更友好的API,适用于复杂的字节码操作。

7.4 反射机制的未来发展趋势

反射机制的未来发展趋势包括以下几个方面:

  • 性能优化:反射机制将继续优化性能,例如通过MethodHandleVarHandle提高调用效率。
  • 安全性增强:反射机制将继续增强安全性,例如通过模块化系统限制反射的访问权限。
  • 新特性支持:反射机制将继续支持新特性,例如泛型、Lambda表达式和密封类。
  • 替代方案的融合:反射机制将与代理机制和字节码操作框架融合,提供更高效的调用方式。

参考文章

posted @ 2022-04-25 15:21  夏尔_717  阅读(1840)  评论(0)    收藏  举报