【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之动态性技术原理指南(反射技术专题)
阅读原文时间:2023年08月10日阅读:2

@

目录

带你攻破你很可能存在的Java技术盲点之动态性技术原理指南

本系列技术专题的相关技术指南主要有以下三个方面:

编程语言的类型

学习一门新的动态类型语言可能需要花费较长的时间,使得已经熟悉Java的开发人员更希望继续使用Java来解决问题。然而,Java本身也支持动态性,在一些需要灵活性的场合可以发挥作用。反射API就是Java中的一个例子,它能够在运行时通过方法名称查找并调用方法。Java语言也在不断更新版本,提高对动态性和灵活性的支持。

整体的编程语言分为三大类:静态类型语言和动态类型语言、半静态半动态类型语言。

Java语言是一种静态类型的编程语言,即要在编译时进行类型检查。在Java中,每个变量的类型需要在声明时显式指定;所有变量、方法的参数和返回值的类型必须在程序运行之前就已经确定。这种静态类型特性使得编译器能够在编译时进行大量的类型检查,从而发现代码中明显的类型错误。然而,这也意味着代码中包含了大量不必要的类型声明,使代码显得过于冗长且不够灵活。相对应的,动态类型语言(如JavaScript和Ruby等)的类型检查则是在运行时进行的。在这类语言中,源代码中的变量类型可以在运行时动态确定。

相比于静态类型语言,动态类型语言(如JavaScript和Ruby等)的类型检查是在运行时进行的。在这类语言中,源代码中不需要显式地声明类型,因此,使用动态类型语言编写的代码更加简洁。近年来,动态类型语言的流行也反映了语言中动态性的重要性。适当的动态性对于提高开发效率非常有帮助,因为它可以减少开发人员需要编写的代码量。

技术核心方向

虽然Java是一种静态类型语言,但是它也提供了使代码更具灵活性的动态性特性。这些特性包括脚本语言支持API、反射API、动态代理和JSR292中引入的动态语言支持。开发人员可以选择不同的方式来提高代码的灵活性。例如,可以使用脚本语言支持API将脚本语言集成到Java程序中,使用反射API在运行时动态调用方法,使用动态代理拦截接口方法调用,或使用JSR292中的方法句柄来实现更多的功能。方法句柄支持多种变换操作,并能满足不同场合的需求。

反射API是Java语言提供的动态性支持,它允许程序在运行时获取Java类的内部结构,如构造方法、域和方法等,并与它们进行交互。反射API也能实现许多动态语言常用的实用功能。按照面向对象的思路,应该通过方法来改变对象的状态,而不是直接修改属性的值。Java类中的属性设置和获取方法名通常遵循JavaBeans规范,以setXxx和getXxx命名。因此,可以编写一个工具类,用于设置和获取任何符合JavaBeans规范的对象的属性。

可以使用Java的反射API实现与JavaScript语言的实现类似的功能,代码量上并不太有差别。实现思路是先从对象的类中查找方法,再调用该方法并传入参数。这个静态方法可以被作为一个实用工具方法在程序中使用。

public class ReflectSetter
   public static void invokeSetter(Object obj,String field,Object value) throws NoSuchMethodException,InvocationTargetException,IllegalAccessException{
     String methodName "set"+field.substring(0,1).toUppercase() + field.substring(1);
     class<?>clazz obj.getclass();
     Method method clazz.getMethod (methodName,value.getclass ())
     method.invoke (obj,value);
 }
}

从上述示例可以看出,反射API可以实现Java语言的灵活使用。实际上,反射API定义了提供者和使用者之间的松散契约,这种契约可以在方法调用时只需要建立在名称和参数类型上,而不需要在代码中首先声明变量。这种方式提供了更大的灵活性和动态性,但也需要开发者自己保证调用的合法性。如果方法调用不合法,相关的异常会在运行时抛出。

反射API常用于方法名或属性名按照特定规则变化的情况:

  • 在Servlet中,利用反射API可以遍历HTTP请求中的所有参数,然后用invokeSetter方法填充领域对象的属性值。
  • 在数据库操作中,也通过反射API实现从查询结果集中创建并填充领域对象的场景。这些对应关系都可以通过反射API来建立。

反射功能操作

反射API虽然能为Java程序带来灵活性,但其实现机制也会带来性能代价。通过反射调用方法一般比直接在源代码中编写的方式慢一到两个数量级。虽然随着Java虚拟机的改进,反射API的性能得到了提升,但在一些对性能要求高的应用中,需要慎用反射API。

获取构造器

可以通过反射API获取Java类中的构造方法,从而在运行时动态地创建Java对象。具体步骤如下:

  1. 获取Class类的对象,可以使用Class.forName方法或者类的.class属性。

  2. 通过Class类的getConstructors方法获取所有的公开构造方法的列表,或者使用getConstructor方法根据参数类型获取公开的构造方法。如果需要获取类中真正声明的构造方法,可以使用getDeclaredConstructors和getDeclaredConstructor方法。

  3. 得到表示构造方法的java.lang.reflect.Constructor对象之后,可以通过其getName方法获取构造方法的名称,getParameterTypes方法获取构造方法的参数类型,getModifiers方法获取构造方法的修饰符等信息。

  4. 最后,可以使用newInstance方法创建出新的对象,该方法接受一个可变参数列表,用于传递构造方法的参数值。如果构造方法没有参数,则可以直接调用newInstance方法。

需要注意的是,使用反射API创建对象的效率较低,应该尽量避免在性能要求较高的场景中使用。

一般的构造方法的获取和使用并没有什么特殊之处,需要特别说明的是对参数长度可变的构造方法和嵌套类(nested class)的构造方法的使用。

长度可变的参数 - 构造方法

如果一个构造方法声明了长度可变的参数,需要使用对应的数组类型的 Class 对象来获取该构造方法,因为长度可变的参数实际上是通过数组来实现的。

使用反射 API 获取参数长度可变的构造方法

例如,如果一个类 VarargsConstructor 的构造方法包含 String 类型的可变长度参数,调用getDeclaredConstructor 方法时需要使用 String[].class,否则会找不到该构造方法。在调用newInstance 方法时,需要将作为实际参数的字符串数组先转换为 Object 类型,以避免方法调用时的歧义,这样编译器就知道将该字符串数组作为一个可变长度的参数来传递。

public class VarargsConstructor {
    public VarargsConstructor(String... names) {}
}

public void useVarargsConstructor() throws Exception {
    Constructor<VarargsConstructor> constructor = VarargsConstructor.class.
        getDeclaredConstructor(String[].class);
    constructor.newInstance((Object) new String[]{"A", "B", "C"});
}

获取嵌套类的构造方法时,需要区分静态和非静态两种情况。

静态嵌套类,可以按照一般的方式来使用。

非静态嵌套类,其特殊之处在于它的对象实例中都有一个隐含的对象引用,指向包含它的外部类对象。这个隐含的对象引用的存在,使得非静态嵌套类中的代码可以直接引用外部类中包含的私有域和方法。因此,在获取非静态嵌套类的构造方法时,类型参数列表的第一个值必须是外部类的 Class 对象。

例如,对于非静态嵌套类 NestedClass,获取其构造方法时需要传入外部类的 Class 对象作为第一个参数,以便在创建新对象时传递外部对象的引用。

static class StaticNestedClass {
    public StaticNestedClass(String name) {}
}
class NestedClass {
    public NestedClass(int count) {}
}
public void useNestedClassConstructor() throws Exception {
    Constructor< StaticNestedClass> sncc = StaticNestedClass.class. getDeclaredConstructor(String.class);
    sncc.newInstance("Alex");
    Constructor<NestedClass> ncc = NestedClass.class.getDeclaredConstructor(ConstructorUsage.class, int.class);
    NestedClass ic = ncc.newInstance(this, 3);
}

获取Field域

通过反射 API,可以获取类中的域(field),包括公开的静态域和对象中的实例域。获取表示域的 java.lang.reflect.Field 类的对象之后,就可以获取和设置域的值。与获取构造方法的方法类似,Class 类中也有 4 个方法用来获取域,分别是 getFields、getField、getDeclaredFields 和 getDeclaredField。

  • getFields 方法返回公开的静态域和对象中的实例域;
  • getField 方法返回指定名称的公开的静态域或对象中的实例域;
  • getDeclaredFields 方法返回类中所有的域,包括私有的静态域和对象中的实例域;
  • getDeclaredField 方法返回指定名称的域,包括私有的静态域和对象中的实例域。
使用反射 API 获取和使用静态域和实例域

获取和使用静态域和实例域的示例,两者的区别在于使用静态域时不需要提供具体的对象实例,使用 null 即可

Field 类中除了操作 Object 的 get 和 set 方法之外,还有操作基本类型的对应方法,包括 getBoolean / setBoolean、getByte / setByte、getChar / setChar、getDouble / setDouble、getFloat / setFloat、getInt / setInt 和 getLong / setLong 等

public void useField() throws Exception {
    Field fieldCount = FieldContainer.class.getDeclaredField("count");
    fieldCount.set(null, 3);
    Field fieldName = FieldContainer.class.getDeclaredField("name");
    FieldContainer fieldContainer = new FieldContainer();
    fieldName.set(fieldContainer, "Bob");
}

总的来说,获取和设置类中的公开域比较简单,但是无法通过反射 API 获取或操作私有域。

获取Method方法

最常使用反射 API 的场景是获取对象中的方法,并在运行时调用该方法。Class 类中有 4 个方法用来获取方法,分别是 getMethods、getMethod、getDeclaredMethods 和 getDeclaredMethod。这些方法的作用类似于获取构造方法和域的对应方法。通过获取表示方法的 java.lang.reflect.Method 类的对象,可以查询该方法的详细信息,例如方法的参数和返回值的类型等。使用 invoke 方法可以传入实际参数并调用该方法。

获取和调用对象中的公开和私有方法的示例
public void useMethod() throws Exception {
    MethodContainer mc = new MethodContainer();
    Method publicMethod = MethodContainer.class.getDeclaredMethod("publicMethod");
    publicMethod.invoke(mc);
    Method privateMethod = MethodContainer.class.getDeclaredMethod("privateMethod");
    privateMethod.setAccessible(true);
    privateMethod.invoke(mc);
}

需要注意的是,在调用私有方法之前,需要先调用 Method 类的setAccessible方法来设置可以访问的权限。与构造方法和域不同的是,通过反射 API 可以获取到类中的私有方法。

操作数组

利用反射API对数组进行操作的方式有所不同于一般的Java对象。需要使用java.lang.reflect.Array这个实用工具类来实现。该类提供了创建数组和操作数组元素的方法。newInstance方法用来创建新的数组。第一个参数是数组中元素的类型,后面的参数是数组的维度信息。

String[] names = ( Array.newInstance(int.class, 3, 3, 3);
double[][][] arrays= (double[][][]) Array.newInstance(double[][].class, 2, 2);
使用反射 API 操作数组

例如,可以使用下面的示例代码创建一个长度为10的一维String数组和一个3x3x3的三维数组:

public void useArray() {
    String[] names = (String[]) Array.newInstance(String.class, 10);
    names[0] = "Hello";
    Array.set(names, 1, "World");
    String str = (String) Array.get(names, 0);
    int[][][] matrix1 = (int[][][]) Array.newInstance(int.class, 3, 3, 3);
    matrix1[0][0][0] = 1;
    int[][][] matrix2 = (int[][][]) Array.newInstance(int[].class, 3, 4);
    matrix2[0][0] = new int[10];
    matrix2[0][1] = new int[3];
    matrix2[0][0][1] = 1;
}

需要注意的是,尽管在创建时只声明了两个维度,但是matrix2实际上也是一个三维数组,因为它的元素类型是double。

访问权限与异常处理

使用反射 API 可以绕过 Java 语言中默认的访问控制权限,例如访问在另一个类中声明的私有方法。这是通过调用继承自 java.lang.reflect.AccessibleObject 的 setAccessible 方法来实现的。在使用 invoke 方法调用方法时,如果方法本身抛出异常,invoke 方法会抛出 InvocationTargetException 异常来表示这种情况。可以通过 InvocationTargetException 异常的 getCause 方法获取真正的异常信息来进行调试。

在 Java 7 中,所有与反射操作相关的异常类都添加了一个新的父类 java.lang.ReflectiveOperationException,可以直接捕获这个新的异常。

内容总结

Java反射技术允许程序在运行时动态地获取类的信息、调用类的方法、访问类的属性等,从而提高程序的灵活性和可扩展性。它可以获取类的名称、包名、父类、接口、构造方法、方法、属性等信息,创建对象,调用方法,访问属性,实现动态代理等功能。Java反射技术在框架开发、ORM框架、动态代理、单元测试等方面都有着重要的应用。但是,由于使用反射技术需要额外的开销,因此在性能要求较高的场景下,应该尽量避免使用。