泛型与如何调用泛型方法

泛型与如何调用泛型方法

泛型是Java 5开始添加的语法特性,Java实现泛型的方式叫作“类型擦除”。Java语言中的泛型只在程序源码中存在,在编译后的class文件,全部泛型都被替换为原来的裸类型,如List被替换为List。在调用泛型方法且返回值类型是泛型时,编译器会在调用方法的字节码指令之后插入一条强制类型转换的字节码指令,将方法返回值强转为相应类型。

Signature属性

编译器在将泛型擦除时会创建相应的Signature属性,如为泛型类在class文件结构的属性表中添加一个Signature属性、为泛型方法在方法的属性表中添加一个Signature属性、为泛型字段在字段的属性表中添加一个Signature属性。

Signature属性并不是虚拟机执行字节码指令所必须的,只是有助于反射、调试以及反编译。根据《Java虚拟机规范》规定,Signature属性的结构为:

Signature_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 signature_index;
}

◆attribute_name_index:值为常量池中表示字符串“Signature”的CONSTANT_Utf8_info常量的索引;

◆attribute_length:属性长度,值固定为2;

◆signature_index:值为常量池中某个CONSTANT_Utf8_info常量的索引,用于表示类签名或者方法类型签名或字段类型签名。

例如泛型类MyArrayList:

public class MyArrayList<T> extends ArrayList<T> {

private T tmp;

    @Override
    public boolean add(T t) {
        return super.add(t);
    }
}

编译后为类生成的Signature属性表示类的签名:

<T:Ljava/lang/Object;>Ljava/util/ArrayList<TT;>;

编译后为泛型方法add生成的Signature属性表示方法签名:

(TT;)Z

add方法的方法描述符为:

(Ljava/lang/Object;)Z

编译后为泛型字段tmp生成的Signature属性表示字段类型签名:

TT;

tmp字段的类型描述符为:

Ljava/lang/Object;

Java语言中的泛型特性支持泛型界定。例如MyArrayList,我们希望它的泛型类型是Number类型的子类,则可以改为MyArrayList,代码如下:

public class MyArrayList<T extends Number> extends ArrayList<T> {

    private T tmp;

    @Override
    public boolean add(T t) {
        return super.add(t);
    }
}

编译后为类生成的Signature属性表示类签名:

<T:Ljava/lang/Number;>Ljava/util/ArrayList<TT;>;

add方法的方法描述符为:

(Ljava/lang/Number;)Z

tmp字段的类型描述符为:

Ljava/lang/Number;

从这个例子可以看出,MyArrayList<T>与MyArrayList<T extends Number>类型擦除后都是MyArrayList;MyArrayList<T>的add方法类型描述符为“(Ljava/lang/Object;)Z”,MyArrayList<T extends Number>的add方法类型描述符为“(Ljava/lang/Number;)Z”;MyArrayList<T>的tmp字段类型描述符为“Ljava/lang/Object;”,MyArrayList<T extends Number>的tmp字段类型描述符为“Ljava/lang/Number;”。

因此,我们在使用ASM生成一个泛型类时,如果想生成泛型类MyArrayList<T>,如果不需要生成Signature属性,那么我们可以生成一个MyArrayList类,并为该类添加一个名为tmp且类型为Object的字段,添加一个方法名为add且方法描述符为“(Ljava/lang/Object;)Z”的方法。在不需要生成Signature属性的情况下,生成一个泛型类其实与生成一个普通的类并无区别。

LocalVariableTypeTable属性

LocalVariableTypeTable属性与Signature属性一样,都是为支持泛型而添加的,也不是虚拟机执行字节码指令所必须的。LocalVariableTypeTable属性在调试时用来确定某个局部变量的值。

LocalVariableTypeTable属性的结构为:

LocalVariableTypeTable_attribute{
   u2 attribute_name_index;
   u4 attribute_length;
   u2 local_variable_type_table_length;
   {
   u2 start_pc
   u2 length;
   u2 name_index;
   u2 signature_index;
   u2 index;
 }local_variable_type_table[local_variable_type_table_length];
}

◆attribute_name_index:值为常量池中表示字符串“Signature”的CONSTANT_Utf8_info常量的索引;

◆attribute_length:属性长度,不包括初始的6个字节;

◆local_variable_type_table_length:local_variable_type_table数组的成员数量;

◆local_variable_type_table:数组中的每一项都以偏移量方式给出code[]数组中某个范围,当局部变量处于这个范围内的时候,它是有值的,且还会给出局部变量在当前栈映射桢的局部变量表中的位置。

例如:

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("hello word!");
        String one = list.get(0);
    }

编译后生成的属性如下:

LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      21     1  list   Ljava/util/List<Ljava/lang/String;>;

即局部变量表索引为1的局部变量,在方法的code[]数组的[8,8+21) 范围内,该局部变量必定存储某个值,局部变量的名称为“list”,类型签名为“Ljava/util/List<Ljava/lang/String;>;”。

如何调用泛型方法

以List<T>的add方法为例,由于List<T>经类型擦除后变为List,且List<T>中字段类型为T的字段都会被修改类型为Object,List<T>中有声明参数类型为T或者返回值类型为T的方法,该方法的类型为T的参数类型或者类型为T的返回值类型都会被替换为Object类型。因此,调用一个泛型方法只需要知道该方法经过编译后生成的方法描述符,与调用一个非泛型方法一样调用即可。

例如:

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("hello word!");
        String one = list.get(0);
    }

使用javap命令输出这段代码的字节码如下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String hello word!
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: iconst_0
        19: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        24: checkcast     #7                  // class java/lang/String
        27: astore_2
        28: return

从方法的字节码可以看出,在调用List的get方法后面加了一条强制类型转换的字节码指令,将List的get方法返回值由Object类型强制转为String类型。使用ASM框架生成例子中的main方法,如代码清单8-1所示。

代码清单8-1 调用泛型方法

private static void createMainMethod(ClassWriter cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC,
                "main",
                "([Ljava/lang/String;)V",
                null, null);
        mv.visitCode();
        // List list = new ArrayList<>;
        mv.visitTypeInsn(NEW, Type.getInternalName(ArrayList.class));
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(ArrayList.class), "<init>", "()V", false);
        mv.visitVarInsn(ASTORE, 1);

        // list.add("hello word!")
        mv.visitVarInsn(ALOAD, 1);
        mv.visitLdcInsn("hello word!");
        mv.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(List.class), "add", "(Ljava/lang/String;)V", true);

        // String str = list.get(0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitInsn(ICONST_0);
        mv.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(List.class), "get", "(I)Ljava/lang/String;", true);
        mv.visitTypeInsn(CHECKCAST, Type.getInternalName(String.class));
        mv.visitVarInsn(ASTORE, 2);

        mv.visitInsn(RETURN);
        mv.visitFrame(0, 0, null, 0, null);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

代码清单8-1中,我们使用MethodVisitor的visitTypeInsn方法将当前操作数栈顶元素的类型转为String类型。CHECKCAST指令的操作码为0xC0,调用visitTypeInsn方法时第二个参数传递转换后的类型的内部类名。这段代码生成的Java代码如下:

public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add("hello word!");
        String var2 = (String)var1.get(0);
    }

因此,在使用ASM框架操作字节码时,我们可以去掉泛型的概念,不需要考虑泛型的存在。例如,当我们清楚的知道一个方法的返回值类型是Long类型,但方法描述符描述该方法的返回值类型是Object时,我们可以使用强制类型转换指令将方法的返回值由Object类型强转为Long类型。