泛型是 Java 开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码。本文总结了 Java 泛型的三种形式以及泛型实现原理。
1. 泛型
泛型的本质是对类型进行参数化,在代码逻辑不关注具体的数据类型时使用。例如:实现一个通用的排序算法,此时关注的是算法本身,而非排序的对象的类型。
1.1 泛型方法
如下定义了一个泛型方法,<t> 声明了一个类型变量,它可以应用于参数,返回值,和方法内的代码逻辑。</t>
class GenericMethod{
public <T> T[] sort(T[] elements){
return elements;
}
}
1.2 泛型类
与泛型方法类似,泛型类也需要声明类型变量,只不过位置放在了类名后面,作用的范围包括了当前中的成员变量类型,方法参数类型,方法返回类型,以及方法内的代码中。
子类继承泛型类时或者实例化泛型类的对象时,需要指定具体的参数类型或者声明一个参数变量。如下,SubGenericClass 继承了泛型类 GenericClass,其中类型变量 ID 的值为 Integer,同时子类声明了另一个类型变量 E,并将E 填入了父类声明的 T 中。
class GenericClass<ID, T>{
}
class SubGenericClass<T> extends GenericClass<Integer, T>{
}
1.3 泛型接口
泛型接口与泛型类类似,也需要在接口名后面声明类型变量,作用于接口中的抽象方法返回类型和参数类型。子类在实现泛型接口时需要填入具体的数据类型或者填入子类声明的类型变量。
interface GenericInterface<T> {
T append(T seg);
}
2. 泛型的基本原理
泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。
Java 编译器通过如下方式实现擦除:
- 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
- 在恰当的位置插入强制转换代码来确保类型安全;
- 在继承了泛型类或接口的类中插入桥接方法来保留多态性。
Java 官方文档原文
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
下面通过具体代码来说明 Java 中的类型擦除。
实验原理:先用 javac 将 .java 文件编译成 .class 文件,再使用反编译工具 jad 将 .class 文件反编成回 Java 代码,反编译出来的 Java 代码内容反映的即为 .class 文件中的信息。
如下源代码,定义 User 类,实现了 Comparable 接口,类型参数填入 User,实现 compareTo 方法。
class User implements Comparable<User> {
String name;
public int compareTo(User other){
return this.name.compareTo(other.name);
}
}
JDK 中 Comparable 接口源码内容如下:
package java.lang;
public interface Comparable<T>{
int compareTo(T o);
}
我们首先反编译它的接口,Comparable 接口的字节码文件,可以在 $JRE_HOME/lib/rt.jar
中找到,将它复制到某个目录。使用 jad.exe(需要另外安装)反编译这个 Comparable.class 文件。
$ jad Comparable.class
反编译出来的内容放在 Comparable.jad 文件中,文件内容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Comparable.java
package java.lang;
// Referenced classes of package java.lang:
// Object
public interface Comparable
{
public abstract int compareTo(Object obj);
}
对比源代码 Comparable.java 和反编译代码 Comparable.jad 的内容不难发现,反编译之后的内容中已经没有了类型变量 T 。compareTo 方法中的参数类型 T 也被替换成了 Object。这就符合上面提到的第 1 条擦除原则。这里演示的是用 Object 替换类型参数,使用界定类型替换类型参数的例子可以反编译一下 Collections.class 试试,里面使用了大量的泛型。
使用 javac.exe 将 User.java 编译成 .class 文件,然后使用 jad 将 .class 文件反编译成 Java 代码。
$ javac User.java
$ jad User.class
User.jad 文件内容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: User.java
class User
implements Comparable
{
User()
{
}
public int compareTo(User user)
{
return name.compareTo(user.name);
}
// 桥接方法
public volatile int compareTo(Object obj)
{
return compareTo((User)obj);
}
String name;
}
对比编辑的源代码 User.java 和反编译出来的代码 User.jad,容易发现:类型参数没有了,多了一个无参构造方法,多了一个 compareTo(Object obj) 方法,这个就是桥接方法,还可以发现参数 obj 被强转成 User 再传入 compareTo(User user) 方法。通过这些内容可以看到擦除规则 2 和规则 3 的实现方式。
强转规则比较好理解,因为泛型被替换成了 Object,要调用具体类型的方法或者成员变量,当然需要先强转成具体类型才能使用。那么插入的桥接方法该如何理解呢?
如果我们只按照下面方式去使用 User 类,这样确实不需要参数类型为 Object 的桥接方法。
User user = new User();
User other = new User();
user.comparetTo(other);
但是,Java 中的多态特性允许我们使用一个父类或者接口的引用指向一个子类对象。
Comparable<User> user = new User();
而按照 Object 替换泛型参数原则,Comparable 接口中只有 compareTo(Object) 方法,假设没有桥接方法,显然如下代码是不能运行的。所以 Java 编译器需要为子类(泛型类的子类或泛型接口的实现类)中使用了泛型的方法额外生成一个桥接方法,通过这个方法来保证 Java 中的多态特性。
Comparable<User> user = new User();
Object other = new User();
user.compareTo(other);
而普通类中的泛型方法在进行类型擦除时不会产生桥接方法。例如:
class Dog{
<T> void eat(T[] food){
}
}
类型擦除之后变成了:
class Dog
{
Dog()
{
}
void eat(Object aobj[])
{
}
}
3. 小结
Java 中的泛型有 3 种形式,泛型方法,泛型类,泛型接口。Java 通过在编译时类型擦除的方式来实现泛型。擦除时使用 Object 或者界定类型替代泛型,同时在要调用具体类型方法或者成员变量的时候插入强转代码,为了保证多态特性,Java 编译器还会为泛型类的子类生成桥接方法。类型信息在编译阶段被擦除之后,程序在运行期间无法获取类型参数所对应的具体类型。
网友评论