Java泛型(一)类型擦除

前言

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

C++模板

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

#include 

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

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

int main(int argc, const char * argv[]) {
    HasF hf;
    Manipulator 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 {
    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 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
Java泛型(一)类型擦除_第1张图片

从上面报错信息可以看出,编译器认为类型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().getClass();
        Class c2 = new ArrayList().getClass();
        System.out.println(c1 == c2);
    }
}

果然,执行结果为

true

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

Java泛型(一)类型擦除_第2张图片

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

public class Manipulator2 {
    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 {
    private T obj;
    public void set(T obj) { this.obj = obj; }
    public T get() { return obj; }
    public static void main(String[] args) {
        GenericHolder 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."":()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 "":()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 {
    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 {
    Class kind;
    public ClassTypeCapture(Class kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
        ClassTypeCapture ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture 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 create(); }

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

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

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

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

模板方法模式。

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

class X {}

class Creater extends GenericWithCreate {
    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 {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class 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 gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

真的完全擦除了吗

开启新世界的大门

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

你可能感兴趣的:(Java泛型(一)类型擦除)