67、Java反射:泛型类型检查、数组操作与代理类的深入解析

Java反射:泛型类型检查、数组操作与代理类的深入解析

1. 泛型类型检查

在Java编程中,存在多种接口用于表示程序里不同类型的类型。之前我们主要关注 Class 对象和 Member 对象,因为它们是更常用的反射对象,而其他类型的 Type 对象只是简单提及。下面我们详细探讨这些其他的 Type 接口。

1.1 类型变量

Class Method Constructor 实现了 GenericDeclaration 接口,该接口有一个 getTypeParameters 方法,用于返回 TypeVariable 对象数组。 TypeVariable 接口本身是一个泛型接口,其声明如下:

interface TypeVariable<D extends GenericDeclaration>

例如, Method.getTypeParameters 返回的 TypeVariable 对象类型为 TypeVariable<Method>

每个类型变量都有一个通过 getName 方法返回的名称,还有一个或多个上界,可通过 getBounds 方法获取 Type 数组。若没有显式的上界,上界则为 Object getGenericDeclaration 方法返回声明该 TypeVariable GenericDeclaration Type 对象。例如:

TypeVariable.class.getTypeParameters()[0]

会得到一个 TypeVariable 对象,它代表上述声明中的 D 。对该对象调用 getGenericDeclaration 方法,会返回 TypeVariable 接口的 Class 对象。一般来说,对于任何至少有一个类型参数的 GenericDeclaration 对象 g ,以下表达式总是为真(假设 i 是有效索引):

g.getTypeParameters()[i].getGenericDeclaration() == g

类型变量对象由返回它们的反射方法按需创建,每次请求相同的类型变量时,不要求返回相同的 TypeVariable 对象,但根据 equals 方法,为给定类型变量返回的对象必须相等。类型变量的边界在调用 getBounds 方法时才会创建,因此该方法可能会抛出 TypeNotPresentException (若边界中使用的类型无法找到)或 MalformedParameterizedTypeException (若任何边界引用了因某种原因无法创建的 ParameterizedType 实例)。

1.2 参数化类型

List<String> 这样的参数化类型由实现 ParameterizedType 接口的对象表示。可以通过 getActualTypeArguments 方法获取参数化类型的实际类型参数,该方法返回一个 Type 对象数组。例如,对 List<String> 调用 getActualTypeArguments 方法,会得到一个长度为1的数组,其唯一元素是 String.class

getOwnerType 方法(或许更适合称为 getDeclaringType )返回该 ParameterizedType 对象所属类型的 Type 对象。若它不是其他类型的成员,则返回 null ,这类似于 Class.getDeclaringClass 方法,但返回的是 Type 对象。

TypeVariable 对象一样, ParameterizedType 对象也是按需创建的,不一定是同一个对象,所以应使用 equals 而不是 == 来检查参数化类型对象是否相等。创建参数化类型时,其所有类型参数也会被创建,并且这是递归进行的。上述两个方法有时会抛出 TypeNotFoundException MalformedParameterizedTypeException

最后, ParameterizedType 还有 getrawType 方法,用于返回参数化类型的原始类型的 Class 对象。例如,对 List<String> 调用 getrawType 方法,会返回 List.class 。尽管原始类型按定义是非泛型类或接口,但 getrawType 方法返回的是 Type 实例而非 Class<?> ,所以需要进行强制类型转换。

1.3 通配符

通配符类型参数由实现 WildcardType 接口的实例表示。例如,对于 List<? extends Number> 的参数化类型,调用 getActualTypeArguments 方法会得到一个长度为1的数组,其中包含一个表示 ? extends Number WildcardType 对象。

WildcardType 有两个方法: getUpperBounds getLowerBounds ,分别返回表示通配符上界和下界的 Type 数组。若未指定上界,上界为 Object ;若通配符没有下界, getLowerBounds 方法返回一个空数组。和 TypeVariable 一样,边界的类型对象按需创建,因此可能会抛出 TypeNotPresentException MalformedParameterizedTypeException

1.4 泛型数组

最后一个与类型相关的接口是 GenericArrayType ,它表示组件类型为参数化类型或类型变量的数组类型。 GenericArrayType 有一个 getGenericComponentType 方法,用于返回数组组件类型的 Type ,该类型可能是 ParameterizedType TypeVariable 。例如,对于 List<String>[] 字段, getGenericType 方法会返回一个 GenericArrayType 对象,其 getComponentType 方法会返回 List<String> ParameterizedType

需要注意的是,不能创建这样的数组,但可以声明该类型的变量。实际创建数组时,只能使用无界通配符类型,如 new List<?> [1] 。当首次将新数组赋值给更具体的变量(如 List<String>[] )时,会得到一个“unchecked”警告,因为编译器无法保证数组当前或未来的内容实际上是 List<String> 对象。这种数组本质上不安全,使用时要格外谨慎,一般不应创建返回此类数组或接受它们作为参数的方法。调用 getGenericComponentType 方法时会创建组件类型对象,因此可能会抛出 TypeNotPresentException MalformedParameterizedTypeException

1.5 类型对象的字符串表示

上述接口除了 TypeVariable getName 方法外,都没有定义 toString 方法或获取类型字符串表示的通用方式。不过,所有类型对象都会定义 toString 方法。由于没有规定 toString 方法对 Type 对象应返回什么内容,所以不能依赖它来合理表示类型。若需要类型的字符串表示,需根据可用信息自行组装。例如,若 WildcardType 对象没有下界且上界为 X ,则通配符为 ? extends X 。对于 ParameterizedType ,可以使用原始类型和实际类型参数类型来构造字符串表示。

练习 :使用反射编写一个程序,打印一个命名类的完整声明,除了导入语句、注释以及初始化器、构造函数和方法的代码外,成员声明应与手动编写的一样。需要使用所有见过的反射类,并且要注意,许多反射对象的 toString 方法可能无法以正确格式提供所需信息,因此需要将各个信息片段拼凑起来。

2. 数组操作

数组是对象,但没有成员,数组的隐式长度“字段”并非实际字段。向数组的 Class 对象询问字段、方法或构造函数,都会得到空数组。要创建数组以及获取和设置数组中存储元素的值,可以使用 Array 类的静态方法。

2.1 创建数组

Array 类提供了两种 newInstance 方法来创建数组:

public static Object newInstance(Class<?> componentType, int length)

返回指定长度且组件类型为 componentType 的新数组的引用。例如:

byte[] ba = (byte[]) Array.newInstance(byte.class, 13);

等同于:

byte[] ba = new byte[13];

另一个方法:

public static Object newInstance(Class<?> componentType, int[] dimensions)

返回一个多维数组的引用,其维度由 dimensions 数组的元素指定,组件类型为 componentType 。若 dimensions 数组为空或长度大于实现允许的维度数(通常为255),会抛出 IllegalArgumentException 。例如:

int[] dims = { 4, 4 };
double[][] matrix = (double[][]) Array.newInstance(double.class, dims);

等同于:

double[][] matrix = new double[4][4];

由于组件类型本身可能是数组类型,所以创建的数组的实际维度可能大于 newInstance 方法参数所暗示的维度。例如,若 intArray int[] 类型的 Class 对象,调用 Array.newInstance(intArray, 13) 会创建一个二维的 int[][] 数组。当 componentType 是数组类型时,创建的数组的组件类型是 componentType 的组件类型,所以在上述示例中,结果的组件类型是 int

2.2 获取数组长度

Array 类的静态 getLength 方法用于返回给定数组的长度。

2.3 获取和设置数组元素

Array 类还有静态方法用于获取和设置指定数组的单个元素,类似于 Field 类的 get set 方法。通用的 get set 方法处理 Object 类型。例如,对于 int 数组 xa ,可以通过以下方式获取 xa[i] 的值:

Array.get(xa, i)

该方法返回一个 Integer 对象,需要拆箱才能提取 int 值。设置值的方式类似:

xa[i] = 23;

等同于:

Array.set(xa, i, 23);

若作为数组传递的对象实际上不是数组,会抛出 IllegalArgumentException ;若要设置的值在必要时拆箱后不能赋值给数组的组件类型,也会抛出该异常。

Array 类还为所有基本类型提供了完整的 getType setType 方法,例如:

Array.setInt(xa, i, 23);

这样可以避免使用中间包装对象。

2.4 泛型与动态数组

回顾第11章中 SingleLinkQueue 类的 toArray 方法,之前承诺会展示如何通过传入队列实际类型参数的类型标记直接创建数组,而不是让调用者传入数组。以下是第一次尝试:

public E[] toArray_v1(Class<E> type) {
    int size = size();
    E[] arr = (E[]) Array.newInstance(type, size);
    int i = 0;
    for (Cell<E> c = head; c != null && i < size; c = c.getNext())
        arr[i++] = c.getElement();
    return arr;
}

这段代码可以工作,但不如接受数组作为参数的泛型版本理想。主要问题是它会导致编译器发出“unchecked”警告。因为对 E[] 的强制类型转换涉及类型参数,在运行时实际会转换为 Object (即 E 的擦除类型)。尽管如此,上述代码在类型上是安全的:我们请求一个组件类型为 E 的数组,并尝试将返回的对象用作这样的数组。“unchecked”警告的存在是 Array API的限制导致的,无法避免。

另一个问题是,它和原始的非泛型 toArray 方法有同样的局限性,即只能创建元素类型完全匹配的数组,不能创建任何超类型的数组。可以将当前版本转换为泛型方法来解决这个问题:

public <T> T[] toArray(Class<T> type) {
    int size = size();
    T[] arr = (T[]) Array.newInstance(type, size);
    int i = 0;
    Object[] tmp = arr;
    for (Cell<E> c = head; c != null && i < size; c = c.getNext())
        tmp[i++] = c.getElement();
    return arr;
}

这个版本仍然有“unchecked”警告(无法避免),但允许传入任何 Class 对象,并尝试返回具有该组件类型的数组。和接受数组作为参数的泛型版本一样,这个版本依赖于数组存储的运行时检查,以确保传入的组件类型实际上与当前队列的元素类型兼容。

因此,处理 toArray 需求有两种方法:一是让调用者传入数组,避免警告,但可能需要处理大小不合适的数组;二是让调用者传入元素类型的类型标记,创建大小合适的数组,但会收到“unchecked”警告。或者可以像集合类那样将两者结合:接受一个数组,若大小不合适则动态创建另一个数组,并接受警告。虽然通常建议尽量避免“unchecked”警告,但 Array.newInstance 的情况是个例外。

练习 :进一步修改 Interpret 程序,允许用户指定要创建的数组的类型和大小,设置和获取数组元素,以及访问数组特定元素的字段并调用其方法。

3. 包操作

可以对 Class 对象调用 getPackage 方法,得到一个 Package 对象,它描述了该类所在的包( Package 类在 java.lang 中定义)。也可以通过传入包名调用静态方法 getPackage 来获取 Package 对象,或者使用静态 getPackages 方法返回系统中所有已知包的数组。 getName 方法返回包的完整名称。

Package 对象的使用方式与其他反射类型不同,不能在运行时创建或操作包,而是用于获取包的相关信息,如包的用途、创建者、版本等。

4. 代理类

Proxy 类允许在运行时创建实现一个或多个接口的类。这是一个高级且不常用的特性,但在需要时非常有用。

假设要记录对某个对象的调用,以便在发生故障时打印该对象上最后调用的几个方法。可以为特定类手动编写这样的代码,并为特定对象开启记录功能,但这需要为每个要监控的对象类型编写自定义代码,并且每个对象在每次方法调用时都要检查是否应记录调用。

可以编写一个通用工具,使用 Proxy 创建的类来记录调用历史。该类创建的对象将实现相关接口,并在调用者调用方法和对象执行方法之间插入提供的代码。

Proxy 的使用模式是:调用 Proxy.getProxyClass 方法,传入类加载器和接口数组,获取代理的 Class 对象。代理对象有一个构造函数,需要传入一个 InvocationHandler 对象。可以从 Class 对象获取该构造函数的 Constructor 对象,并使用 newInstance 方法(传入调用处理程序)创建代理对象。创建的对象实现了传递给 getProxyClass 方法的所有接口以及 Object 类的方法。作为获取代理对象的快捷方式,可以调用 Proxy.newProxyInstance 方法,它接受类加载器、接口数组和调用处理程序。当在代理对象上调用方法时,这些方法调用会转换为对调用处理程序的 invoke 方法的调用。

以下是一个通用的调试日志类示例:

import java.lang.reflect.*;
import java.util.*;

public class DebugProxy implements InvocationHandler {
    private final Object obj;           // 底层对象
    private final List<Method> methods; // 调用的方法
    private final List<Method> history; // 可查看的历史记录

    private DebugProxy(Object obj) {
        this.obj = obj;
        methods = new ArrayList<Method>();
        history = Collections.unmodifiableList(methods);
    }

    public static synchronized Object proxyFor(Object obj) {
        Class<?> objClass = obj.getClass();
        return Proxy.newProxyInstance(
            objClass.getClassLoader(),
            objClass.getInterfaces(),
            new DebugProxy(obj));
    }

    public Object
        invoke(Object proxy, Method method, Object[] args)
        throws Throwable
    {
        methods.add(method); // 记录调用
        try {
            // 调用实际方法
            return method.invoke(obj, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }

    public List<Method> getHistory() { return history; }
}

若需要为给定对象创建调试代理,可以调用 proxyFor 方法:

Object proxyObj = DebugProxy.proxyFor(realObj);

proxyObj 对象将实现 realObj 实现的所有接口以及 Object 类的方法。它还与创建的 DebugProxy 实例关联,该实例是代理的调用处理程序。当在 proxyObj 上调用方法时,会调用关联的 DebugProxy 实例的 invoke 方法,传入 proxyObj 作为代理对象、表示被调用方法的 Method 对象以及方法调用的所有参数。在示例中, invoke 方法将调用记录添加到已调用方法列表中,然后在底层的 realObj 对象上调用该方法。

可以通过将代理传递给 Proxy 类的静态 getInvocationHandler 方法来获取代理对象的方法调用历史记录:

DebugProxy h = (DebugProxy) Proxy.getInvocationHandler(proxyObj);
List<Method> history = h.getHistory();

若不使用 newProxyInstance 快捷方式,在 ProxyFor 方法中需要编写以下代码:

Class<?> objClass = obj.getClass();
Class<?> proxyClass = Proxy.getProxyClass(
    objClass.getClassLoader(),
    objClass.getInterfaces());
Constructor ctor = proxyClass.getConstructor(
    InvocationHandler.class);
return ctor.newInstance(new DebugProxy(obj));

调用处理程序的 invoke 方法可以抛出 Throwable 异常。若 invoke 方法抛出原始方法无法抛出的异常,调用者将得到一个 UndeclaredThrowableException ,可以通过其 getCause 方法获取引发异常的异常。

若两次使用相同参数(相同的类加载器和相同顺序的相同接口)调用 getProxyClass 方法,会得到相同的 Class 对象;若接口顺序不同或类加载器不同,则会得到不同的 Class 对象。接口顺序很重要,因为列表中的两个接口可能有名称和签名相同的方法。若发生这种情况,传递给 invoke 方法的 Method 对象的声明类将是列表中第一个声明该方法的接口(通过接口和超接口的深度优先搜索定义)。

Object 类的公共非最终方法( equals hashCode toString )的声明类始终是 Object.class Object 类的其他方法不会被“代理”,它们的方法由代理对象直接处理,而不是通过调用 invoke 方法。最重要的是,对代理对象的锁只是对代理的锁,代理用于完成工作的任何对象(例如,在我们的示例中是被跟踪方法的底层对象)不参与锁操作,包括 wait notifyAll notify 的任何使用。

可以使用 Proxy 类的静态 isProxyClass 方法询问一个 Class 对象是否表示动态生成的代理类。

通过以上对泛型类型检查、数组操作、包操作和代理类的详细介绍,我们可以更深入地理解Java反射机制在不同场景下的应用,从而在编程中灵活运用这些特性来实现各种功能。

Java反射:泛型类型检查、数组操作与代理类的深入解析

5. 总结与实践建议
5.1 泛型类型检查总结
  • 类型变量 :通过 GenericDeclaration 接口的 getTypeParameters 方法获取 TypeVariable 对象数组。类型变量有名称、上界,其对象按需创建,使用 equals 方法判断相等。调用 getBounds 方法时可能抛出异常。
  • 参数化类型 :由实现 ParameterizedType 接口的对象表示,通过 getActualTypeArguments 获取实际类型参数, getOwnerType 获取所属类型, getrawType 获取原始类型。对象按需创建,使用 equals 判断相等,创建时会递归创建类型参数,相关方法可能抛出异常。
  • 通配符 :由实现 WildcardType 接口的实例表示,通过 getUpperBounds getLowerBounds 获取上下界,边界类型对象按需创建,可能抛出异常。
  • 泛型数组 GenericArrayType 表示组件类型为参数化类型或类型变量的数组,使用 getGenericComponentType 获取组件类型,创建和使用时需谨慎,调用方法可能抛出异常。
  • 字符串表示 :除 TypeVariable getName 方法外,需自行根据信息组装类型的字符串表示。

实践建议 :在处理泛型类型时,要注意异常处理,特别是在获取类型边界和创建类型对象时。使用 equals 方法比较类型对象,避免使用 ==

5.2 数组操作总结
  • 创建数组 :使用 Array 类的 newInstance 方法,有两种重载形式,可创建一维和多维数组。组件类型可以是数组类型,创建时可能抛出 IllegalArgumentException
  • 获取长度 :使用 Array 类的 getLength 方法。
  • 获取和设置元素 :使用 Array 类的 get set 方法或基本类型的 getType setType 方法,操作时需注意对象类型和元素类型的兼容性,可能抛出 IllegalArgumentException
  • 泛型与动态数组 :使用 Array.newInstance 创建泛型数组时会有“unchecked”警告,但在某些情况下可结合不同方式处理 toArray 需求。

实践建议 :创建数组时要确保参数的合法性,避免异常。在处理泛型数组时,可根据实际情况选择合适的方法,对于“unchecked”警告可使用 @SuppressWarnings 注解。

5.3 包操作总结

通过 Class 对象的 getPackage 方法、 getPackage 静态方法或 getPackages 静态方法获取 Package 对象,用于获取包的相关信息,不能在运行时创建或操作包。

实践建议 :在需要了解类所在包的信息时,使用相应方法获取 Package 对象进行查询。

5.4 代理类总结

Proxy 类允许在运行时创建实现一个或多个接口的类,通过 getProxyClass newProxyInstance 方法创建代理对象,代理对象的方法调用会转换为对 InvocationHandler invoke 方法的调用。注意接口顺序、异常处理和锁的使用。

实践建议 :在需要记录方法调用、插入额外逻辑等场景下使用代理类。在实现 InvocationHandler invoke 方法时,要正确处理异常,避免影响原方法的正常执行。

6. 常见问题及解决方案
6.1 泛型类型检查相关问题
  • 问题 :调用 getBounds getActualTypeArguments 等方法时抛出 TypeNotPresentException MalformedParameterizedTypeException
  • 解决方案 :检查类型名称是否正确,确保相关类在类路径中。对于 MalformedParameterizedTypeException ,检查类型参数的使用是否正确。
6.2 数组操作相关问题
  • 问题 :创建数组时抛出 IllegalArgumentException
  • 解决方案 :检查 newInstance 方法的参数,确保 dimensions 数组不为空且长度不超过实现允许的维度数,传入的对象确实是数组类型,设置的值与数组组件类型兼容。
  • 问题 :使用 Array.newInstance 创建泛型数组时出现“unchecked”警告。
  • 解决方案 :可使用 @SuppressWarnings("unchecked") 注解抑制警告,或结合不同方式处理 toArray 需求。
6.3 代理类相关问题
  • 问题 invoke 方法抛出 UndeclaredThrowableException
  • 解决方案 :检查 invoke 方法中抛出的异常是否是原始方法可以抛出的,处理好异常的捕获和抛出。
  • 问题 :代理对象的锁操作不符合预期。
  • 解决方案 :理解代理对象的锁只是对代理的锁,不涉及代理用于工作的其他对象,避免在锁操作上产生混淆。
7. 流程图展示

以下是创建代理对象的流程图:

graph TD;
    A[开始] --> B[获取对象的类加载器和接口数组];
    B --> C{选择创建方式};
    C -- 使用getProxyClass --> D[调用Proxy.getProxyClass获取Class对象];
    C -- 使用newProxyInstance --> E[调用Proxy.newProxyInstance获取代理对象];
    D --> F[获取构造函数的Constructor对象];
    F --> G[传入InvocationHandler调用newInstance创建代理对象];
    E --> H[创建完成];
    G --> H;
    H --> I[结束];
8. 表格总结
操作类型 关键方法 作用 可能抛出的异常
泛型类型检查 - 类型变量 getTypeParameters 获取类型变量数组
getName 获取类型变量名称
getBounds 获取类型变量上界 TypeNotPresentException MalformedParameterizedTypeException
getGenericDeclaration 获取声明类型变量的 GenericDeclaration Type 对象
泛型类型检查 - 参数化类型 getActualTypeArguments 获取实际类型参数 TypeNotFoundException MalformedParameterizedTypeException
getOwnerType 获取所属类型的 Type 对象 TypeNotFoundException MalformedParameterizedTypeException
getrawType 获取原始类型的 Class 对象
泛型类型检查 - 通配符 getUpperBounds 获取通配符上界 TypeNotPresentException MalformedParameterizedTypeException
getLowerBounds 获取通配符下界 TypeNotPresentException MalformedParameterizedTypeException
泛型类型检查 - 泛型数组 getGenericComponentType 获取数组组件类型 TypeNotPresentException MalformedParameterizedTypeException
数组操作 - 创建数组 newInstance(Class<?> componentType, int length) 创建一维数组 IllegalArgumentException
newInstance(Class<?> componentType, int[] dimensions) 创建多维数组 IllegalArgumentException
数组操作 - 获取长度 getLength 获取数组长度
数组操作 - 获取和设置元素 get set 通用的获取和设置元素方法 IllegalArgumentException
getType setType 基本类型的获取和设置元素方法 IllegalArgumentException
代理类 getProxyClass 获取代理的 Class 对象
newProxyInstance 快捷获取代理对象
invoke 处理代理对象的方法调用 Throwable

通过对上述内容的学习和实践,我们可以更好地掌握Java反射机制在泛型类型检查、数组操作和代理类等方面的应用,提高编程的灵活性和效率。同时,要注意处理好各种异常情况,避免程序出现错误。在实际开发中,根据具体需求选择合适的技术和方法,充分发挥Java反射的强大功能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值