# 4.1 泛型
诸如 List<E>
之类的泛型类,以及使用它们的类,包含了有关它们所声明或使用的泛型的信息。这一信息不是由字节代码指令在运行时使用,但可通过反射 API 访问。它还可以供编译器使用,以进行分离编译。
# 4.1.1 结构
出于后向兼容的原因,有关泛型的信息没有存储在类型或方法描述符中(它们的定义远早于Java 5 中对泛型的引入),而是保存在称为类型、方法和类签名的类似构造中。在涉及泛型时,除了描述符之外,这些签名也会存储在类、字段和方法声明中(泛型不会影响方法的字节代码: 编译器用它们执行静态类型检查,但会在必要时重新引入类型转换,就像这些方法未被使用一样进行编译)。
与类型和方法描述符不同,类型签名的语法非常复杂,这也是因为泛型的递归本质造成的(一个泛型可以将另一泛型作为参数——例如,考虑 List<List<E>>
)。其语法由以下规则给出(有关这些规则的完整描述,请参阅 《Java 虚拟机规范》):
TypeSignature: Z | C | B | S | I | F | J | D | FieldTypeSignature FieldTypeSignature: ClassTypeSignature | [ TypeSignature | TypeVar ClassTypeSignature: L Id ( / Id )* TypeArgs? ( . Id TypeArgs? )* ; TypeArgs: < TypeArg+ > TypeArg: * | ( + | - )? FieldTypeSignature TypeVar: T Id ;
@小傅哥: 代码已经复制到剪贴板
2
3
4
5
6
7
第一条规则表明,类型签名或者是一个基元类型描述符,或者是一个字段类型签名。第二条规则将一个字段类型签名定义为一个类类型签名、数组类型签名或类型变量。第三条规则定义类类型签名:它们是类类型描述符,在主类名之后或者内部类名之后的尖括号中可能带有类型参数 (以点为前缀)。其他规则定义了类型参数和类型变量。注意,一个类型参数可能是一个完整的字段类型签名,带有它自己的类型参数:因此,类型签名可能非常复杂(见图 4.1)。
Java 类型 | 相应的类型签名 |
---|---|
List<E> | Ljava/util/List<TE;>; |
List<?> | Ljava/util/List<*>; |
List<? extends Number> | Ljava/util/List<+Ljava/lang/Number;>; |
List<? super Integer> | Ljava/util/List<-Ljava/lang/Integer;>; |
List<List<String>[]> | Ljava/util/List<[Ljava/util/List<Ljava/lang/String;>;>; |
HashMap<K, V>.HashIterator<K> | Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>; |
方法签名扩展了方法描述符,就像类型签名扩展了类型描述符。方法签名描述了方法参数的类型签名及其返回类型的签名。与方法描述符不同的是,它还包含了该方法所抛出异常的签名, 前面带有^前缀,还可以在尖括号之间包含可选的形式类型参数:
MethodTypeSignature: TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception* Exception: ^ClassTypeSignature | ^TypeVar TypeParams: < TypeParam+ > TypeParam: Id : FieldTypeSignature? ( : FieldTypeSignature )*
@小傅哥: 代码已经复制到剪贴板
2
3
4
5
比如以下泛型静态方法的方法签名,它以类型变量 T 为参数:
static <T> Class<? extends T> m (int n)
它是以下方法签名:
<T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;
最后要说的是类签名,不要将它与类类型签名相混淆,它被定义为其超类的类型签名,后面跟有所实现接口的类型签名,以及可选的形式类型参数:
ClassSignature: TypeParams? ClassTypeSignature ClassTypeSignature*
例 如 , 一 个 被 声 明 为 C<E> extends List<E>
的 类 的 类 签 名 就 是 <E:Ljava/lang/Object;>Ljava/util/List<TE;>;
。
# 4.1.2 接口与组件
和描述符的情况一样,也出于相同的效果原因(见 2.3.1 节),ASM API 公开签名的形式与它们在编译类中的存储形式相同(签名主要出现在 ClassVisitor 类的 visit、visitField 和 visitMethod 方法中,分别作为可选类、类型或方法签名参数 )。幸好它还在 org.objectweb.asm.signature 包中提供了一些基于 SignatureVisitor 抽象类的工具,用于生成和转换签名(见图 4.2)。
图 4.2 SignatureVisitor 类
public abstract class SignatureVisitor { public final static char EXTENDS = ’+’; public final static char SUPER = ’-’; public final static char INSTANCEOF = ’=’; public SignatureVisitor(int api); public void visitFormalTypeParameter(String name); public SignatureVisitor visitClassBound(); public SignatureVisitor visitInterfaceBound(); public SignatureVisitor visitSuperclass(); public SignatureVisitor visitInterface(); public SignatureVisitor visitParameterType(); public SignatureVisitor visitReturnType(); public SignatureVisitor visitExceptionType(); public void visitBaseType(char descriptor); public void visitTypeVariable(String name); public SignatureVisitor visitArrayType(); public void visitClassType(String name); public void visitInnerClassType(String name); public void visitTypeArgument(); public SignatureVisitor visitTypeArgument(char wildcard); public void visitEnd(); }
@小傅哥: 代码已经复制到剪贴板
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
这个抽象类用于访问类型签名、方法签名和类签名。用于类型签名的方法以粗体显示,必须按以下顺序调用,它反映了前面的语法规则(注意,其中两个返回了 SignatureVisitor:这是因为类型签名的递归定义导致的):
visitBaseType | visitArrayType | visitTypeVariable | ( visitClassType visitTypeArgument* ( visitInnerClassType visitTypeArgument* )* visitEnd ) )
@小傅哥: 代码已经复制到剪贴板
2
用于访问方法签名的方法如下:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitParameterType* visitReturnType visitExceptionType*
@小傅哥: 代码已经复制到剪贴板
2
最后,用于访问类签名的方法为:
( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* visitSuperClass visitInterface*
@小傅哥: 代码已经复制到剪贴板
2
这些方法大多返回一个 SignatureVisitor:它是准备用来访问类型签名的。注意,不同于 ClassVisitor 返 回 的 MethodVisitors , SignatureVisitor 返 回 的 SignatureVisitors 不得为 null,而且必须顺序使用:事实上,在完全访问一个嵌套签名之前,不得访问父访问器的任何方法。
和类的情况一样,ASM API 基于这个 API 提供了两个组件:SignatureReader 组件分析一个签名,并针对一个给定的签名访问器调用适当的访问方法;SignatureWriter 组件基于它接收到的方法调用生成一个签名。
利用与类和方法相同的原理,这两个类可用于生成和转换签名。例如,假定我们希望对出现在某些签名中的类名进行重命名。这一效果可以用以下签名适配器完成,除 visitClassType 和 visitInnerClassType 方法之外,它将自己接收到的所有其他方法调用都不加修改地加以转发(这里假设 sv 方法总是返回 this,SignatureWriter 就属于这种情况):
public class RenameSignatureAdapter extends SignatureVisitor { private SignatureVisitor sv; private Map<String, String> renaming; private String oldName; public RenameSignatureAdapter(SignatureVisitor sv, Map<String, String> renaming) { super(ASM4); this.sv = sv; this.renaming = renaming; } public void visitFormalTypeParameter(String name) { sv.visitFormalTypeParameter(name); } public SignatureVisitor visitClassBound() { sv.visitClassBound(); return this; } public SignatureVisitor visitInterfaceBound() { sv.visitInterfaceBound(); return this; } ... public void visitClassType(String name) { oldName = name; String newName = renaming.get(oldName); sv.visitClassType(newName == null ? name : newName); } public void visitInnerClassType(String name) { oldName = oldName + "." + name; String newName = renaming.get(oldName); sv.visitInnerClassType(newName == null ? name : newName); } public void visitTypeArgument() { sv.visitTypeArgument(); } public SignatureVisitor visitTypeArgument(char wildcard) { sv.visitTypeArgument(wildcard); return this; } public void visitEnd() { sv.visitEnd(); } }
@小傅哥: 代码已经复制到剪贴板
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
因此,以下代码的结果为"LA<TK;TV;>.B<TK;>;"
:
String s = "Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;"; Map<String, String> renaming = new HashMap<String, String>(); renaming.put("java/util/HashMap", "A"); renaming.put("java/util/HashMap.HashIterator", "B"); SignatureWriter sw = new SignatureWriter(); SignatureVisitor sa = new RenameSignatureAdapter(sw, renaming); SignatureReader sr = new SignatureReader(s); sr.acceptType(sa); sw.toString();
@小傅哥: 代码已经复制到剪贴板
2
3
4
5
6
7
8
9
# 4.1.3 工具
2.3 节给出的TraceClassVisitor 和 ASMifier 类以内部形式打印类文件中包含的签名。利用它们,可以通过以下方式找出与一个给定泛型相对应的签名:编写一个具有某一泛型的 Java 类,编译它,并用这些命令行工具来找出对应的签名。