ttttwr

导航

Java中的“伪泛型”

Java中的“伪泛型”

Java与C#的泛型

在编写Java泛型的过程中,我们发现:
如果我们编写了一个泛型MyGenerics,在执行一下代码时,输出的结果为true:

public class Main {
    public static void main(String[] args) {
        var myGenerics = new MyGenerics<>(10);
        var myGenerics2 = new MyGenerics<>("Hello");
        System.out.println(myGenerics.getClass()); // class MyGenerics
        System.out.println(myGenerics2.getClass()); // class MyGenerics
        System.out.println(myGenerics.getClass() == myGenerics2.getClass());
    }
}

class MyGenerics<T> {
    private T t;
    public MyGenerics(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

但是凭直觉来看,显然这两个泛型是不一样的,对其类型的比较理应返回的是false。不过如果我们使用C#编写相似的代码如下,其返回值就是符合我们的直觉的false:

var myGenerics = new MyGenerics<int>(10);
var myGenerics2 = new MyGenerics<string>("Hello");

Console.WriteLine(myGenerics.GetType()); // MyGenerics`1[System.Int32]
Console.WriteLine(myGenerics2.GetType()); // MyGenerics`1[System.String]
Console.WriteLine(myGenerics.GetType() == myGenerics2.GetType());

internal class MyGenerics<T>(T value)
{
    public T Value { get; set; } = value;
}

那么为什么会出现这种情况呢,这其实是Java与C#对泛型的实现方法不同造成的。
简单来说,Java的泛型是依靠“类型擦除”这一方法实现的,其原理就是在编译时,将(大多数)泛型信息擦除,只剩下原始类型。因此在字节码中或者说对于JVM来说,泛型在代码中其实是不存在的,例如我们编写并实例化了一个MyGenerics泛型,在字节码中,实际上只有MyGenerics这一个类,而对于String的类型转换、操作都被直接写死在了字节码中。
上面的解释还是有些抽象,接下来我们用实际的字节码/中间代码来解释。
为了更好地展示,我们编写了如下Java代码:

public class Main {
    public static void main(String[] args) {
        var myGenerics = new MyGenerics<>(10);
        int num = myGenerics.getT() + 20;
        System.out.println(num);
    }
}

class MyGenerics<T> {
    private T t;
    public MyGenerics(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

反编译生成的Main.class与MyGenerics.class文件,结果如下:

Compiled from "Main.java"
class MyGenerics<T> {
  public MyGenerics(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #7                  // Field t:Ljava/lang/Object;
       9: return

  public T getT();
    Code:
       0: aload_0
       1: getfield      #7                  // Field t:Ljava/lang/Object;
       4: areturn

  public void setT(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #7                  // Field t:Ljava/lang/Object;
       5: return
}

public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class MyGenerics
       3: dup
       4: bipush        20
       6: invokestatic  #9                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       9: invokespecial #15                 // Method MyGenerics."<init>":(Ljava/lang/Object;)V
      12: astore_1
      13: aload_1
      14: invokevirtual #18                 // Method MyGenerics.getT:()Ljava/lang/Object;
      17: checkcast     #10                 // class java/lang/Integer
      20: invokevirtual #22                 // Method java/lang/Integer.intValue:()I
      23: bipush        10
      25: iadd
      26: istore_2
      27: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
      30: iload_2
      31: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V
      34: return
}

可以发现,MyGenerics中没有任何和泛型相关的东西,全部都使用了Object,而对其中泛型的操作全部都变成了强制类型转换(checkcast),这就是类型擦除方法的效果。
在了解了Java的类型擦除实现之后,我们再来看C#的“真泛型”的实现方法吧,考虑以下代码:

var myGenerics = new MyGenerics<int>(10);
int num = myGenerics.Value + 20;
Console.WriteLine(num);

internal class MyGenerics<T>(T value)
{
    public T Value { get; set; } = value;
}

其生成的IL为:

.class /*02000002*/ private auto ansi beforefieldinit Program
       extends [System.Runtime/*23000001*/]System.Object/*0100000E*/
{
  .custom /*0C00000F:0A00000C*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000C */ = ( 01 00 00 00 ) 
  .method /*06000001*/ private hidebysig static 
          void  '<Main>$'(string[] args) cil managed
  // SIG: 00 01 01 1D 0E
  {
    .entrypoint
    // Method begins at RVA 0x2050
    // Code size       26 (0x1a)
    .maxstack  2
    .locals /*11000001*/ init (class MyGenerics`1/*02000003*/<int32> V_0,
             int32 V_1)
    IL_0000:  /* 1F   | 0A               */ ldc.i4.s   10
    IL_0002:  /* 73   | (0A)000010       */ newobj     instance void class MyGenerics`1/*02000003*/<int32>/*1B000001*/::.ctor(!0) /* 0A000010 */
    IL_0007:  /* 0A   |                  */ stloc.0
    IL_0008:  /* 06   |                  */ ldloc.0
    IL_0009:  /* 6F   | (0A)000011       */ callvirt   instance !0 class MyGenerics`1/*02000003*/<int32>/*1B000001*/::get_Value() /* 0A000011 */
    IL_000e:  /* 1F   | 14               */ ldc.i4.s   20
    IL_0010:  /* 58   |                  */ add
    IL_0011:  /* 0B   |                  */ stloc.1
    IL_0012:  /* 07   |                  */ ldloc.1
    IL_0013:  /* 28   | (0A)000012       */ call       void [System.Console/*23000002*/]System.Console/*01000013*/::WriteLine(int32) /* 0A000012 */
    IL_0018:  /* 00   |                  */ nop
    IL_0019:  /* 2A   |                  */ ret
  } // end of method Program::'<Main>$'

  .method /*06000002*/ public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  // SIG: 20 00 01
  {
    // Method begins at RVA 0x2076
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 28   | (0A)000013       */ call       instance void [System.Runtime/*23000001*/]System.Object/*0100000E*/::.ctor() /* 0A000013 */
    IL_0006:  /* 00   |                  */ nop
    IL_0007:  /* 2A   |                  */ ret
  } // end of method Program::.ctor

} // end of class Program

.class /*02000003*/ private auto ansi beforefieldinit MyGenerics`1<T>
       extends [System.Runtime/*23000001*/]System.Object/*0100000E*/
{
  .custom /*0C000010:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.NullableContextAttribute/*0100000F*/::.ctor(uint8) /* 0A00000D */ = ( 01 00 01 00 00 ) 
  .custom /*0C000011:0A00000E*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.NullableAttribute/*01000010*/::.ctor(uint8) /* 0A00000E */ = ( 01 00 00 00 00 ) 
  .param type T /*2A000001*/ 
    .custom /*0C00000E:0A00000E*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.NullableAttribute/*01000010*/::.ctor(uint8) /* 0A00000E */ = ( 01 00 02 00 00 ) 
  .field /*04000001*/ private !T '<Value>k__BackingField'
  .custom /*0C000001:0A00000C*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000C */ = ( 01 00 00 00 ) 
  .custom /*0C000002:0A00000F*/ instance void [System.Runtime/*23000001*/]System.Diagnostics.DebuggerBrowsableAttribute/*01000012*/::.ctor(valuetype [System.Runtime/*23000001*/]System.Diagnostics.DebuggerBrowsableState/*01000011*/) /* 0A00000F */ = ( 01 00 00 00 00 00 00 00 ) 
  .method /*06000003*/ public hidebysig specialname rtspecialname 
          instance void  .ctor(!T 'value') cil managed
  // SIG: 20 01 01 13 00
  {
    // Method begins at RVA 0x207f
    // Code size       15 (0xf)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 03   |                  */ ldarg.1
    IL_0002:  /* 7D   | (0A)000014       */ stfld      !0 class MyGenerics`1/*02000003*/<!T>/*1B000002*/::'<Value>k__BackingField' /* 0A000014 */
    IL_0007:  /* 02   |                  */ ldarg.0
    IL_0008:  /* 28   | (0A)000013       */ call       instance void [System.Runtime/*23000001*/]System.Object/*0100000E*/::.ctor() /* 0A000013 */
    IL_000d:  /* 00   |                  */ nop
    IL_000e:  /* 2A   |                  */ ret
  } // end of method MyGenerics`1::.ctor

  .method /*06000004*/ public hidebysig specialname 
          instance !T  get_Value() cil managed
  // SIG: 20 00 13 00
  {
    .custom /*0C000012:0A00000C*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000C */ = ( 01 00 00 00 ) 
    // Method begins at RVA 0x208f
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 7B   | (0A)000014       */ ldfld      !0 class MyGenerics`1/*02000003*/<!T>/*1B000002*/::'<Value>k__BackingField' /* 0A000014 */
    IL_0006:  /* 2A   |                  */ ret
  } // end of method MyGenerics`1::get_Value

  .method /*06000005*/ public hidebysig specialname 
          instance void  set_Value(!T 'value') cil managed
  // SIG: 20 01 01 13 00
  {
    .custom /*0C000013:0A00000C*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000C */ = ( 01 00 00 00 ) 
    // Method begins at RVA 0x2097
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 03   |                  */ ldarg.1
    IL_0002:  /* 7D   | (0A)000014       */ stfld      !0 class MyGenerics`1/*02000003*/<!T>/*1B000002*/::'<Value>k__BackingField' /* 0A000014 */
    IL_0007:  /* 2A   |                  */ ret
  } // end of method MyGenerics`1::set_Value

  .property /*17000001*/ instance callconv(8) !T
          Value()
  {
    .get instance !T MyGenerics`1/*02000003*/::get_Value() /* 06000004 */
    .set instance void MyGenerics`1/*02000003*/::set_Value(!T) /* 06000005 */
  } // end of property MyGenerics`1::Value
} // end of class MyGenerics`1

由于IL较长,而且可读性不高,我们挑出其中最有代表性的几句来看:

# 对应Main中myGenerics.Value
MyGenerics`1/*02000003*/<int32>/*1B000001*/::get_Value() /* 0A000011 */
# 对应MyGenerics<T>中get
MyGenerics`1/*02000003*/<!T>/*1B000002*/::'<Value>k__BackingField' /* 0A000014 */

可以看出,C#的IL代码完全保留了泛型,这也就是所谓的“真泛型”,称为类型膨胀。无论在程序源码中、编译后的IL中(这时泛型是一个占位符)或是运行期的CLR中都是真实存在的。

思考与总结

两种泛型实现方法

  • C#中使用了“类型膨胀”来实现泛型,其特点是泛型在源代码、中间代码和运行中都是存在的,对于泛型T不同的两个对象来说,它们的类型是不同的。
  • Java中使用了“类型擦除”来实现泛型,其特点是泛型实际上只存在于源代码中(字节码中还是存在一些个别的记号),在字节码、运行中,泛型实际上是不存在的,在编译的过程中,泛型被替换为原始类型(Raw Type,也称为裸类型),并且在相应的地方插入了强制转型代码,而在原来的泛型内部的T都被替换成了Object。

Java的“伪泛型”带来的影响

在实验中,有时我们需要为泛型编写equals函数。由于euqals函数的传入参数是Object类型,因此在equals函数的开头,我们一般都会先判断Object与当前类型是否相等,但是在泛型中,如果我们使用这种方法,其实并不能严谨地判断二者是否相同,这正是因为泛型在运行中都是原始类型,并不能区分泛型T是否相同。当然,Java的“伪泛型”还带来了很多其他的缺点,在这里就不一一赘述了,总而言之,这一泛型实现方式确实会为初学者带来一些烦恼与不解。
当然,这并不是说Java就无法处理这种情况了,我们可以在其后判断其中包含的Object是否相同时弥补这个问题。同样,这也并不意味着JVM无法实现“真泛型”,之所以Java没有实现真泛型,更多的可能还是由于历史原因,在Java5中才首次引入了泛型,这时考虑到保持兼容性,同时Java代码量已经较大,最终Java选择了使用类型擦除来实现泛型,而.Net(C#)在加入泛型的时候这两个问题还不是十分严重,因此它选择了实现“真泛型”,最终才造成了如今的“分立”的结果。

posted on 2024-05-10 17:38  TTTTWR  阅读(17)  评论(0)    收藏  举报