Java™ 5 扩展了 Java 语言类型系统以支持类、方法和值的参数化类型。参数化的类型通过确保使用正确的类型及消除从源代码进行类型转换提供了重要的编译时好处。除了这些编译时好处,类型信息对于 classworking 工具操纵 Java 代码也有帮助。在本文中,JiBX 首席开发员 Dennis Sosnoski 分析了如何用反射深入参数化类型的内部,并充分展示了 Java 5 应用程序数据结构的优势。
许多工具都是围绕使用 Java 反射而设计的,它们的用途包括从用数据值填充 GUI 组件到在运行的应用程序中动态装载新功能。反射对于在运行时分析数据结构特别有用,许多在内部对象结构与外部格式(包括 XML、数据库和其他持久化格式)之间转换的框架都基于对数据结构的反射分析。
使用反射分析数据结构的一个问题是标准 Java 集合类(如 java.util.ArrayList
)对于反射来说总是“死胡同(dead-end)” —— 到达一个集合类后,无法再访问数据结构的更多细节,因为没有关于集合中包含的项目类型的信息。Java 5 改变了这一情况,它增加了对泛型的支持,将所有集合类转换为支持类型的泛型形式。Java 5 还扩展了反射 API ,支持在运行时对泛型类型信息进行访问。这些改变使反射可以比以往更深入地挖掘数据结构。
许多文章讨论了 Java 5 的泛型功能的使用(包括 参考资料 中的链接)。对于本文,假定您已经了解泛型的基本知识。我们首先使用一些示例代码,然后直接讨论如何在运行时访问泛型信息。
作为使用泛型的一个例子,我准备使用一个表示一组路径中的目录和文件的数据结构。清单 1 给出了这个数据结构根类的代码。PathDirectory
类取路径 String
数组作为构造函数参数。这个构造函数将每一个字符串解释为目录路径,并构造一个数据结构以表示这个路径下面的文件和子目录。处理每一路径时,这个构造函数就将这个路径和这个路径的数据结构加到一个成对集合(pair collection)中。
public class PathDirectory implements Iterable<String> { private final PairCollection<String, DirInfo> m_pathPairs; public PathDirectory(String[] paths) { m_pathPairs = new PairCollection<String, DirInfo>(); for (String path : paths) { File file = new File(path); if (file.exists() && file.isDirectory()) { DirInfo info = new DirInfo(new File(path)); m_pathPairs.add(path, info); } } } public PairCollection<String, DirInfo>.PairIterator iterator() { return m_pathPairs.iterator(); } public static void main(String[] args) { PathDirectory inst = new PathDirectory(args); PairCollection<String, DirInfo>.PairIterator iter = inst.iterator(); while (iter.hasNext()) { String path = iter.next(); DirInfo info = iter.matching(); System.out.println("Directory " + path + " has " + info.getFiles().size() + " files and " + info.getDirectories().size() + " child directories"); } } } |
清单 2 给出了 PairCollection<T,U>
的代码。这个泛型类处理成对的值,类型参数给出了这些对中项目的类型。它提供了一个 add()
方法向集合中加入一个元组(tuple),一个 clear()
方法清空集合中所有元组,一个 iterator()
方法返回遍历集合中所有对的迭代器。内部 PairIterator
类实现由后一个方法返回的特殊迭代器,它定义了一个额外的 matching()
方法,这个方法用于得到由标准 next()
方法返回的值的配对(第二个)值。
public class PairCollection<T,U> implements Iterable<T> { // code assumes random access so force implementation class private final ArrayList<T> m_tValues; private final ArrayList<U> m_uValues; public PairCollection() { m_tValues = new ArrayList<T>(); m_uValues = new ArrayList<U>(); } public void add(T t, U u) { m_tValues.add(t); m_uValues.add(u); } public void clear() { m_tValues.clear(); m_uValues.clear(); } public PairIterator iterator() { return new PairIterator(); } public class PairIterator implements Iterator<T> { private int m_offset; public boolean hasNext() { return m_offset < m_tValues.size(); } public T next() { if (m_offset < m_tValues.size()) { return m_tValues.get(m_offset++); } else { throw new NoSuchElementException(); } } public U matching() { if (m_offset > 0) { return m_uValues.get(m_offset-1); } else { throw new NoSuchElementException(); } } public void remove() { throw new UnsupportedOperationException(); } } } |
PairCollection<T,U>
对于包含值的实际集合在内部使用泛型。它用第一个参数类型实现了 java.lang.Iterable
接口,从而可以直接在新型 for
循环中使用以遍历每对中的第一个值。不幸的是,在使用新型 for
循环时,无法访问实际的迭代器,因而无法获取每一对的第二个值。这就是为什么 清单 1 中的 main()
测试方法使用 while
循环而不是一个新的 for
循环。
清单 3 给出了包含目录和文件信息的一对类的代码。DirInfo
类使用有类型的 java.util.List
集合表示普通文件和目录的子目录。构造函数将这些集合创建为不可修改的列表,使得它们可以安全地直接返回。FileInfo
类更简单,只包含文件名和最后修改日期。
public class DirInfo { private final List<FileInfo> m_files; private final List<DirInfo> m_directories; private final Date m_lastModify; public DirInfo(File dir) { m_lastModify = new Date(dir.lastModified()); File[] childs = dir.listFiles(); List<FileInfo> files = new ArrayList<FileInfo>(); List<DirInfo> dirs = new ArrayList<DirInfo>(); for (int i = 0; i < childs.length; i++) { File child = childs[i]; if (child.isDirectory()) { dirs.add(new DirInfo(child)); } else if (child.isFile()) { files.add(new FileInfo(child)); } } m_files = Collections.unmodifiableList(files); m_directories = Collections.unmodifiableList(dirs); } public List<DirInfo> getDirectories() { return m_directories; } public List<FileInfo> getFiles() { return m_files; } public Date getLastModify() { return m_lastModify; } } public class FileInfo { private final String m_name; private final Date m_lastModify; public FileInfo(File file) { m_name = file.getName(); m_lastModify = new Date(file.lastModified()); } public Date getLastModify() { return m_lastModify; } public String getName() { return m_name; } } |
清单 4 给出了 清单 1 中的 main()
方法的运行示例:
[dennis]$ java -cp . com.sosnoski.generics.PathDirectory /home/dennis/bin /home/dennis/xtools /home/dennis/docs/business Directory /home/dennis/bin has 31 files and 0 child directories Directory /home/dennis/xtools has 0 files and 3 child directories Directory /home/dennis/docs/business has 34 files and 34 child directories |
泛型是在 Java 平台上作为编译时转换实现的。编译器实际上生成与使用非泛型源代码时相同的字节指令,插入运行时类型转换以在每次访问时将值转换为正确的类型。尽管是相同的字节码,但是类型参数信息用 一个新的签名(signature) 属性记录在类模式中。JVM 在装载类时记录这个签名信息,并在运行时通过反射使它可用。在这一节,我将深入挖掘反射 API 如何使类型信息可用的细节。
通过反射访问类型参数信息有些复杂。首先,需要有一个具有所提供类型信息的字段(或者其他可提供类型的办法,如方法参数或者返回类型)。然后可以用 Java 5 新增的 getGenericType()
方法从这个字段的 java.lang.reflect.Field
实例提取特定于泛型的信息。这个新方法返回一个 java.lang.reflect.Type
实例。
惟一的问题是 Type
是一个没有方法的接口。在实例化后,需要检查扩展了 Type
的子接口以了解得到的是什么(以及如何使用它)。Javadocs 列出了四种子接口,我会依次介绍它们。为了方便,我在清单 5 中给出了接口定义。它们都包括在 java.lang.reflect
包中。
interface GenericArrayType extends Type { Type getGenericComponentType(); } interface ParameterizedType extends Type { Type[] getActualTypeArguments(); Type getOwnerType(); Type getRawType(); } interface TypeVariable<D extends GenericDeclaration> extends Type { Type[] getBounds(); D getGenericDeclaration(); String getName(); } interface WildcardType extends Type { Type[] getLowerBounds(); Type[] getUpperBounds(); } |
java.lang.reflect.GenericArrayType
是第一个子接口。这个子接口提供了关于数组类型的信息,数组的组件类型可以是参数化的,也可以是一个类型变量。只定义了一个方法 getGenericComponentType()
,它返回数组组件 Type
。
java.lang.reflect.ParameterizedType
是 Type
的第二个子接口。它提供了关于具有特定类型参数的泛型类型的信息。这个接口定义了三个方法,其中最让人感兴趣的(对于本文来说)是 getActualTypeArguments()
方法。这个方法返回一个 (drum role)数组 . . .还有更多的 Type
实例。返回的 Type
表示原来(未参数化的)类型的实际类型参数。
Type
的第三个子接口是 java.lang.reflect.TypeVariable<D extands GenericDeclaration>
。这个接口给出了表示一个参数类型的变量(如这个类型名中变量 "D")的细节。这个接口定义了三个方法:getBounds()
,它返回(您猜) Type
实例数组;getGenericDeclaration()
,它返回对应于类型变量声明的 java.lang.reflect.GenericDeclaration
接口的实例;getName()
,它返回类型变量在源代码中使用的名字。这些方法都需要进一步说明。因此我将逐一分析它们。
由 getBounds()
方法返回的类型数组定义对于变量的类型所施加的限制。这些限制在源代码中作为在模板变量中以 extends B
(其中 “B” 是某种类型)的格式添加的子句进行声明。很方便, java.lang.reflect.TypeVariable<D extends GenericDeclaration>
本身就给出了这种形式的上界定义的一个例子 —— java.lang.reflect.GenericDeclaration
是类型参数 “D” 的上界,意味着 “D” 必须是扩展或者实现 GenericDeclaration
的类型。
getGenericDeclaration()
方法提供了一种访问声明了 TypeVariable
的 GenericDeclaration
实例的方式。在标准 Java API 中有三个类实现了 GenericDeclaration
:java.lang.Class
、java.lang.reflect.Constructor
和 java.lang.reflect.Method
。这三个类是有意义的,因为参数类型只能在类、构造函数和方法中声明。GenericDeclaration
接口定义了一个方法,它返回在声明中包含的 TypeVariable
的数组。
getName()
方法只是返回与源代码中给出的完全一样的类型变量名。
java.lang.reflect.WildcardType
是 Type
的第四个(也是最后一个)子接口。WildcardType
只定义了两个方法,返回通配类型的下界和上界。在前面,我给出了上界的一个例子,下界也类似,但是它们是通过指定一种类型而定义的,提供的类型必须是它的超接口或者超类。
我在上一节中描述的反射接口提供了解码泛型信息的钩子,但是确定它们有点难度 —— 不管从哪儿开始,每件事都像是循环并回到 java.lang.reflect.Type
。为了展示它们是如何工作的,我将利用 清单 1 代码中的一个例子并对它进行反射。
首先,我尝试访问 清单 1 的 m_pathPairs
字段的类型信息。清单 6 中的代码得到这个字段的泛型类型,检查结果是否为所预期的类型,然后列出原始类型和参数化类型的实际类型参数。在清单 6 的最后以粗体显示了运行这段代码的输出:
public static void main(String[] args) throws Exception {
// get the basic information
Field field =
PathDirectory.class.getDeclaredField("m_pathPairs");
Type gtype = field.getGenericType();
if (gtype instanceof ParameterizedType) {
// list the raw type information
ParameterizedType ptype = (ParameterizedType)gtype;
Type rtype = ptype.getRawType();
System.out.println("rawType is instance of " +
rtype.getClass().getName());
System.out.println(" (" + rtype + ")");
// list the actual type arguments
Type[] targs = ptype.getActualTypeArguments();
System.out.println("actual type arguments are:");
for (int j = 0; j < targs.length; j++) {
System.out.println(" instance of " +
targs[j].getClass().getName() + ":");
System.out.println(" (" + targs[j] + ")");
}
} else {
System.out.println
("getGenericType is not a ParameterizedType!");
}
}
rawType is instance of java.lang.Class (class com.sosnoski.generics.PairCollection) actual type arguments are: instance of java.lang.Class: (class java.lang.String) instance of java.lang.Class: (class com.sosnoski.generics.DirInfo)
|
到目前为止一切都好。m_pathPairs
字段定义为 PairCollection<String, DirInfo>
类型,它匹配反射所访问的类型信息。不过深入实际的参数化类型定义会有些复杂;返回的 Type
实例是一个没有实现任何 Type
子接口的 java.lang.Class
对象。幸运的是,Java 5 Class<T>
类本身提供了一个深入泛型类定义的细节的方法。这个方法是 getTypeParameters()
,它返回一个 TypeVariable<Class<T>>
数组。在清单 7 中,我修改了 清单 6 的代码以使用这个方法,得到的结果同样以粗体显示在代码中:
public static void main(String[] args) throws Exception {
// get the basic information
Field field =
PathDirectory.class.getDeclaredField("m_pathPairs");
ParameterizedType ptype =
(ParameterizedType)field.getGenericType();
Class rclas = (Class)ptype.getRawType();
System.out.println("rawType is class " + rclas.getName());
// list the type variables of the base class
TypeVariable[] tvars = rclas.getTypeParameters();
for (int i = 0; i < tvars.length; i++) {
TypeVariable tvar = tvars[i];
System.out.print(" Type variable " +
tvar.getName() + " with upper bounds [");
Type[] btypes = tvar.getBounds();
for (int j = 0; j < btypes.length; j++) {
if (j > 0) {
System.out.print(" ");
}
System.out.print(btypes[j]);
}
System.out.println("]");
}
// list the actual type arguments
Type[] targs = ptype.getActualTypeArguments();
System.out.print("Actual type arguments are\n (");
for (int j = 0; j < targs.length; j++) {
if (j > 0) {
System.out.print(" ");
}
Class tclas = (Class)targs[j];
System.out.print(tclas.getName());
}
System.out.print(")");
}
rawType is class com.sosnoski.generics.PairCollection Type variable T with upper bounds [class java.lang.Object] Type variable U with upper bounds [class java.lang.Object] Actual type arguments are (java.lang.String com.sosnoski.generics.DirInfo)
|
清单 7 的结果显示了解码的结构。实际的类型参数可以由泛型类定义的类型变量匹配。在下一节,我将做此工作作为递推泛型解码方法的一部分。
在上一节,我快速完成了访问泛型信息的反射方法。现在我将用这些方法构建一个解释泛型的递推处理程序。清单 8 给出了相关的代码:
public class Reflect { private static HashSet<String> s_processed = new HashSet<String>(); private static void describe(String lead, Field field) { // get base and generic types, check kind Class<?> btype = field.getType(); Type gtype = field.getGenericType(); if (gtype instanceof ParameterizedType) { // list basic parameterized type information ParameterizedType ptype = (ParameterizedType)gtype; System.out.println(lead + field.getName() + " is of parameterized type"); System.out.println(lead + ' ' + btype.getName()); // print list of actual types for parameters System.out.print(lead + " using types ("); Type[] actuals = ptype.getActualTypeArguments(); for (int i = 0; i < actuals.length; i++) { if (i > 0) { System.out.print(" "); } Type actual = actuals[i]; if (actual instanceof Class) { System.out.print(((Class)actual).getName()); } else { System.out.print(actuals[i]); } } System.out.println(")"); // analyze all parameter type classes for (int i = 0; i < actuals.length; i++) { Type actual = actuals[i]; if (actual instanceof Class) { analyze(lead, (Class)actual); } } } else if (gtype instanceof GenericArrayType) { // list array type and use component type System.out.println(lead + field.getName() + " is array type " + gtype); gtype = ((GenericArrayType)gtype). getGenericComponentType(); } else { // just list basic information System.out.println(lead + field.getName() + " is of type " + btype.getName()); } // analyze the base type of this field analyze(lead, btype); } private static void analyze(String lead, Class<?> clas) { // substitute component type in case of an array if (clas.isArray()) { clas = clas.getComponentType(); } // make sure class should be expanded String name = clas.getName(); if (!clas.isPrimitive() && !clas.isInterface() && !name.startsWith("java.lang.") && !s_processed.contains(name)) { // print introduction for class s_processed.add(name); System.out.println(lead + "Class " + clas.getName() + " details:"); // process each field of class String indent = lead + ' '; Field[] fields = clas.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; if (!Modifier.isStatic(field.getModifiers())) { describe(indent, field); } } } } public static void main(String[] args) throws Exception { analyze("", PathDirectory.class); } } |
清单 8 中的代码使用两个相互递推的方法进行实际的分析。analyze()
方法取一个类作为参数,通过对每个字段的定义进行必要的处理展开这个类。describe()
方法打印特定字段的类型信息的描述,对在这一过程中它遇到的每一个类调用 analyze()
。每个方法还有一个给出当前缩进字符串的参数,它使每一级类嵌套都缩进一些空间。
清单 9 给出了用 清单 8 中的代码分析 清单 1、清单 2 和 清单 3 中代码的完整结构所生成的输出。
Class com.sosnoski.generics.PathDirectory details: m_pathPairs is of parameterized type com.sosnoski.generics.PairCollection using types (java.lang.String com.sosnoski.generics.DirInfo) Class com.sosnoski.generics.DirInfo details: m_files is of parameterized type java.util.List using types (com.sosnoski.generics.FileInfo) Class com.sosnoski.generics.FileInfo details: m_name is of type java.lang.String m_lastModify is of type java.util.Date Class java.util.Date details: fastTime is of type long cdate is of type sun.util.calendar.BaseCalendar$Date Class sun.util.calendar.BaseCalendar$Date details: cachedYear is of type int cachedFixedDateJan1 is of type long cachedFixedDateNextJan1 is of type long m_directories is of parameterized type java.util.List using types (com.sosnoski.generics.DirInfo) m_lastModify is of type java.util.Date Class com.sosnoski.generics.PairCollection details: m_tValues is of parameterized type java.util.ArrayList using types (T) Class java.util.ArrayList details: elementData is array type E[] size is of type int m_uValues is of parameterized type java.util.ArrayList using types (U) |
清单 9 的输出给出了泛型类型是如何参数化使用的基本情况,包括为在 DirInfo
类中列出的 m_files
和 m_directories
项指定的类型。但当涉及到 PairCollection
类(在底部)时,字段类型只是作为变量给出。对这个字段只显示为变量的原因是由反射提供的泛型类型信息不处理替换 —— 而是由反射代码的使用者处理泛型类中的替换。这项工作并不太困难,因为可以从清单 9 的输出中进行猜测。这里 m_tValues
展开的细节显示 ArrayList
是用 “T” 类型参数化的,而嵌套的 ArrayList
展开显示 elementData
字段是用类型 “E” 参数化的。要在每一个实例中正确关联这些类型,需要在展开的每一阶段跟踪类型变量实际被替换的类型(如前所述,可用 java.lang.Class.getTypeParameters()
方法得到)。在这里,这意味着在 PairCollection
展开中的 “T” 和 m_tValues
ArrayList
展开中的 “E” 替换 java.lang.String
。我不再给出更多的清单,而是将变化细节留给您。
我已在本文中展示了如何在运行时挖掘已编译类的泛型类型信息(至少是基本的,我忽略了像内部类这样的复杂情况以及泛型中一些更复杂的结构)。
作为一个示例应用程序,我准备使用泛型类型信息改进我的 JiBX XML 数据绑定框架中提供的默认绑定生成器。现在绑定生成器不知道在 Java 集合(或者其他无类型的引用)中出现的是什么样的内容,因此生成器让用户修改生成的绑定并增加相应的内容;加入泛型反射代码后,对于使用 Java 5 的用户,生成器将可以从泛型中直接得到类型信息。
但是在 JVM 中装载类以访问泛型类型信息并不总是方便的。对于 JiBX,处理类时最重要的部分是向编译的类表示中加入字节码。为此,JiBX 使用了一个字节码操纵框架(在 JiBX 中是 BCEL,在 JiBX 2.0 中改为 ASM)。对于 JiBX 来说,幸运的是,ASM 框架包含在解析二进制类表示时访问同样的类型信息的钩子,而且可以在生成新类时添加泛型类型信息。下个月,我将介绍 ASM 的分析方式并与在本月讨论的反射支持进行比较。