详解Java 中泛型的实现原理
泛型是Java开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码。本文总结了Java泛型的三种形式以及泛型实现原理。
泛型
泛型的本质是对类型进行参数化,在代码逻辑不关注具体的数据类型时使用。例如:实现一个通用的排序算法,此时关注的是算法本身,而非排序的对象的类型。
泛型方法
如下定义了一个泛型方法,声明了一个类型变量,它可以应用于参数,返回值,和方法内的代码逻辑。
classGenericMethod{ publicT[]sort(T[]elements){ returnelements; } }
泛型类
与泛型方法类似,泛型类也需要声明类型变量,只不过位置放在了类名后面,作用的范围包括了当前中的成员变量类型,方法参数类型,方法返回类型,以及方法内的代码中。
子类继承泛型类时或者实例化泛型类的对象时,需要指定具体的参数类型或者声明一个参数变量。如下,SubGenericClass继承了泛型类GenericClass,其中类型变量ID的值为Integer,同时子类声明了另一个类型变量E,并将E填入了父类声明的T中。
classGenericClass{ } classSubGenericClass extendsGenericClass { }
泛型接口
泛型接口与泛型类类似,也需要在接口名后面声明类型变量,作用于接口中的抽象方法返回类型和参数类型。子类在实现泛型接口时需要填入具体的数据类型或者填入子类声明的类型变量。
interfaceGenericInterface{ Tappend(Tseg); }
泛型的基本原理
泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的.java源代码,在编译生成.class文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对Java编译器可以见,对Java虚拟机不可见。
Java编译器通过如下方式实现擦除:
- 用Object或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
- 在恰当的位置插入强制转换代码来确保类型安全;
- 在继承了泛型类或接口的类中插入桥接方法来保留多态性。
Java官方文档原文
ReplacealltypeparametersingenerictypeswiththeirboundsorObjectifthetypeparametersareunbounded.Theproducedbytecode,therefore,containsonlyordinaryclasses,interfaces,andmethods.
Inserttypecastsifnecessarytopreservetypesafety.
Generatebridgemethodstopreservepolymorphisminextendedgenerictypes.
下面通过具体代码来说明Java中的类型擦除。
实验原理:先用javac将.java文件编译成.class文件,再使用反编译工具jad将.class文件反编成回Java代码,反编译出来的Java代码内容反映的即为.class文件中的信息。
如下源代码,定义User类,实现了Comparable接口,类型参数填入User,实现compareTo方法。
classUserimplementsComparable{ Stringname; publicintcompareTo(Userother){ returnthis.name.compareTo(other.name); } }
JDK中Comparable接口源码内容如下:
packagejava.lang; publicinterfaceComparable{ intcompareTo(To); }
我们首先反编译它的接口,Comparable接口的字节码文件,可以在$JRE_HOME/lib/rt.jar中找到,将它复制到某个目录。使用jad.exe(需要另外安装)反编译这个Comparable.class文件。
$jadComparable.class
反编译出来的内容放在Comparable.jad文件中,文件内容如下:
//DecompiledbyJadv1.5.8g.Copyright2001PavelKouznetsov. //Jadhomepage:http://www.kpdus.com/jad.html //Decompileroptions:packimports(3) //SourceFileName:Comparable.java packagejava.lang; //Referencedclassesofpackagejava.lang: //Object publicinterfaceComparable { publicabstractintcompareTo(Objectobj); }
对比源代码Comparable.java和反编译代码Comparable.jad的内容不难发现,反编译之后的内容中已经没有了类型变量T。compareTo方法中的参数类型T也被替换成了Object。这就符合上面提到的第1条擦除原则。这里演示的是用Object替换类型参数,使用界定类型替换类型参数的例子可以反编译一下Collections.class试试,里面使用了大量的泛型。
使用javac.exe将User.java编译成.class文件,然后使用jad将.class文件反编译成Java代码。
$javacUser.java $jadUser.class
User.jad文件内容如下:
//DecompiledbyJadv1.5.8g.Copyright2001PavelKouznetsov. //Jadhomepage:http://www.kpdus.com/jad.html //Decompileroptions:packimports(3) //SourceFileName:User.java classUser implementsComparable { User() { } publicintcompareTo(Useruser) { returnname.compareTo(user.name); } //桥接方法 publicvolatileintcompareTo(Objectobj) { returncompareTo((User)obj); } Stringname; }
对比编辑的源代码User.java和反编译出来的代码User.jad,容易发现:类型参数没有了,多了一个无参构造方法,多了一个compareTo(Objectobj)方法,这个就是桥接方法,还可以发现参数obj被强转成User再传入compareTo(Useruser)方法。通过这些内容可以看到擦除规则2和规则3的实现方式。
强转规则比较好理解,因为泛型被替换成了Object,要调用具体类型的方法或者成员变量,当然需要先强转成具体类型才能使用。那么插入的桥接方法该如何理解呢?
如果我们只按照下面方式去使用User类,这样确实不需要参数类型为Object的桥接方法。
Useruser=newUser(); Userother=newUser(); user.comparetTo(other);
但是,Java中的多态特性允许我们使用一个父类或者接口的引用指向一个子类对象。
Comparableuser=newUser();
而按照Object替换泛型参数原则,Comparable接口中只有compareTo(Object)方法,假设没有桥接方法,显然如下代码是不能运行的。所以Java编译器需要为子类(泛型类的子类或泛型接口的实现类)中使用了泛型的方法额外生成一个桥接方法,通过这个方法来保证Java中的多态特性。
Comparableuser=newUser(); Objectother=newUser(); user.compareTo(other);
而普通类中的泛型方法在进行类型擦除时不会产生桥接方法。例如:
classDog{voideat(T[]food){ } }
类型擦除之后变成了:
classDog { Dog() { } voideat(Objectaobj[]) { } }
小结
Java中的泛型有3种形式,泛型方法,泛型类,泛型接口。Java通过在编译时类型擦除的方式来实现泛型。擦除时使用Object或者界定类型替代泛型,同时在要调用具体类型方法或者成员变量的时候插入强转代码,为了保证多态特性,Java编译器还会为泛型类的子类生成桥接方法。类型信息在编译阶段被擦除之后,程序在运行期间无法获取类型参数所对应的具体类型。
参考
https://docs.oracle.com/javase/tutorial/java/generics/index.html
https://stackoverflow.com/questions/25040837/generics-bridge-method-on-polymorphism
以上就是详解Java中泛型的实现原理的详细内容,更多关于Java泛型实现原理的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。