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反射的强大功能。
超级会员免费看
4884

被折叠的 条评论
为什么被折叠?



