Java中的“伪泛型”
Java中的“伪泛型”
Java与C#的泛型
在编写Java泛型的过程中,我们发现:
如果我们编写了一个泛型MyGenerics
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
上面的解释还是有些抽象,接下来我们用实际的字节码/中间代码来解释。
为了更好地展示,我们编写了如下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#)在加入泛型的时候这两个问题还不是十分严重,因此它选择了实现“真泛型”,最终才造成了如今的“分立”的结果。
浙公网安备 33010602011771号