Java泛型(一)类型擦除

前言

本文为对Java泛型技术类型擦除部分的一个总结,主要参考文献有《Java编程思想 第4版》、《Java核心技术 第10版》、《深入理解Java虚拟机 第2版》,文中的代码Demo主要来自于Java编程思想。

C++模板

下面是使用模板的C++示例。

#include <iostream>

class HasF {
public:
    void f() { std::cout << "HasF::f()" << std::endl; }
};

template<class T> class Manipulator {
    T obj;
public:
    Manipulator(T x) { obj = x; }
    void manipulate() { obj.f(); }
};

int main(int argc, const char * argv[]) {
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
    return 0;
}

程序运行无误,输出

HasF::f()

当我们调用一个模板时,C++编译器用实参来为我们推断模板实参,并为我们实例化(instantiate)一个特定版本的代码。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。被编译器生成的版本通常称为模板的实例

对于上面代码,在编译时,编译器会将T替换成HasF并生成模板实例,类似于这样。

class Manipulator {
    HasF obj;
public:
    Manipulator(HasF x) { obj = x; }
    void manipulate() { obj.f(); }
};

Java泛型有点不太一样

用C++编写这种代码很简单,因为当模板实例化时,模板代码知道其模板参数的类型,Java泛型就不同了。下面是HasF的Java版本。

// HasF.java
public class HasF {
    public void f() { System.out.println("HasF.f()"); }
}

// Manipulation.java
class Manipulator<T> {
    private T obj;
    public Manipulator(T x) { obj = x; }
    public void manipulate() { obj.f(); } // 编译错误
}
    
public class Manipulation {
    public static void main(String[] args) {
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator(hf);
        manipulator.manipulate();
    }
}

上面代码不能编译,报错

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
    The method f() is undefined for the type T

从上面报错信息可以看出,编译器认为类型T没有f()方法。这是由于Java的泛型是使用擦除来实现的,意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。

从表面上看,Java的泛型类类似于C++的模板类,唯一明显的不同是Java没有专用的template关键字,但是,其实这两种机制着本质的区别

擦除法实现的伪泛型

Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。使用擦除法的好处就是实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。

泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。

为了验证擦除,我们编写下面代码。

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
}

果然,执行结果为

true

尽管ArrayList<String>和ArrayList<Integer>看上去是不同的类型,但是上面的程序会认为它们是相同的类型。ArrayList<String>和ArrayList<Integer>在运行时事实上是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList。无论何时,编写泛型代码时,必须提醒自己“它的类型被擦除了”。

如果想要运行上面Java版本的HasF,必须协助泛型类,给定泛型的边界,以告知编译器只能接收遵循这个边界的类型。这里重用了extends关键字(与类的继承有点类似,但又不完全相同),给出类型的上界。之所以称为上界,是通过继承树来考虑的,对于继承树父节点在上,子节点在下,那么extends关键字就限定了类型最多能上了继承树的什么地方,也就是上界。由于有了边界,下面的代码就可以编译了。

public class Manipulator2<T extends HasF> {
    private T obj;
    public Manipulator2(T x) { obj = x; }
    public void manipulate() { obj.f(); }
}

泛型类型参数将擦除到它的第一个边界(可以有多个边界)。编译时,Java编译器会将T擦除成HasF,就好像在类的声明中用HasF替换了T一样。

那泛型有什么用

可能就有小伙伴疑惑了,上面的泛型好像并没有什么用,我直接写下面这种手动擦除的代码不行吗。

public class Manipulator3 {
    private HasF obj;
    public Manipulator3(HasF x) { obj = x; }
    public void manipulate() { obj.f(); }
}

这提出了很重要的一点:只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加泛化化时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。

泛型参数和他们在有用的泛型代码中的应用,通常比简单的替换来的更复杂。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型。

例如对于下面两种写法,泛型的写法可以不用强制转换类型。编译器会在编译期执行类型检查并插入转型代码。理解编译器对泛型的处理非常重要。

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public void set(Object obj) { this.obj = obj; }
    public Object get() { return obj; }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        Holder.set("Item");
        String string = (String)Holder.get();
    }
}

// GenericHolder.java
public class GenericHolder<T> {
    private T obj;
    public void set(T obj) { this.obj = obj; }
    public T get() { return obj; }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.set("Item");
        String s = holder.get();
    }
}

其实它们生成的字节码是完全相同的。对进入set()的类型检查是不需要的,因为这是由编译器执行的,而对从get()返回的值进行转型仍旧是需要的。

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

public void set(T);
    Code:
        0: aload_0
        1: aload_1
        2: putfield      #2                  // Field obj:Ljava/lang/Object;
        5: return

public T get();
    Code:
        0: aload_0
        1: getfield      #2                  // Field obj:Ljava/lang/Object;
        4: areturn

public static void main(java.lang.String[]);
    Code:
        0: new           #3                  // class GenericHolder
        3: dup
        4: invokespecial #4                  // Method "<init>":()V
        7: astore_1
        8: aload_1
        9: ldc           #5                  // String Item
       11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
       14: aload_1
       15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
       18: checkcast     #8                  // class java/lang/String
       21: astore_2
       22: return

可以看出,使用泛型机制编写的代码要比哪些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型是我们需要的程序设计手段。

解决擦除的问题

擦除丢失了在泛型代码中执行某些操作的能力。任何运行时都需要知道确切的类型信息的操作都无法工作。

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        if (arg instanceof T) {}  // 编译错误
        T var = new T();  // 编译错误
        T[] array = new T[SIZE];  // 编译错误
    }
}

下面给出一些方法解决上面的问题。

  • 引入类型标签,使用动态的isInstance()代替instanceof
class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);     
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}

运行结果

true
true
false
true
  • 用工厂方法或模版方法创建类型实例

下面的两段代码是学习设计模式的好材料。先来看工厂方法模式。

interface FactoryI<T> { T create(); }

class Foo2<T> {
    private T x;
    public <F extends FactoryI<T>> Foo2(F factory) {
        x = factory.create();
    }
}

class IntegerFactory implements FactoryI<Integer> {
    public Integer create() {
        return new Integer(0);
    }
}

class Widget {
    public static class Factory implements FactoryI<Widget> {
        public Widget create() {
            return new Widget();
        }
    }
}

public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2<Integer>(new IntegerFactory());
        new Foo2<Widget>(new Widget.Factory());
    }
}

模板方法模式。

abstract class GenericWithCreate<T> {
    final T element;
    public GenericWithCreate() { element = create(); }
    abstract T create();
}

class X {}

class Creater extends GenericWithCreate<X> {
    X create() { return new X(); }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}
public class CreatorGeneric {
    public static void main(String[] args) {
        Creater c = new Creater();
        c.f();
    }
}
  • 用ArrayList代替数组,或者是传入类型标记
public class GenericArrayWithTypeToken<T> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[])Array.newInstance(type, sz);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    public T[] rep() { return array; }
    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

真的完全擦除了吗

开启新世界的大门

在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 2,971评论 0 2
  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 990评论 0 3
  • 文章作者:Tyan博客:noahsnail.com 1. 什么是泛型 Java泛型(Generics)是JDK 5...
    SnailTyan阅读 761评论 0 3
  • “站着像生煎,走着像油炸,在机坪上是红烧,下机坪后是清蒸,我们是奔跑的五花肉,我们为自己带盐! 这是高温预警下,我...
    我是我自己的骄傲阅读 489评论 4 8
  • 为了你的梦想,飞吧!!! 我要振翅高飞 我想傲视苍穹 我准备好了一切 可是 风来了,雨来了 我湿了翅膀,失了方向...
    慎思笃行月阅读 422评论 0 0