Retrofit源码阅中,发现自带晕眩光环的Type(简直无CD),而谈到Type绕不过泛型,借这个机会好好捋捋。

1.泛型的由来

引入版本: Java平台在JDK 5中引入了一个重要的特性 —— 泛型(generics)(又被称作参数化类型 parameterized type),。

泛型引入的目:为了解决容器(数据结构)的类型安全性,使得编译器在编译时就能发现明显的类型错误,从而避免运行时的转型错误(不细说了)。(泛型的优点不仅如此,见下文)

2.泛型实现原理

通常情况下,一个编译器处理泛型有两种方式:

  • Code specialization。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要 针对string,integer,float产生三份目标代码。
  • Code sharing。对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。

C++中的模板(template)是典型的Code specialization实现。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。
Code specialization会导致:

  • 代码膨胀(code bloat),不过有经验的C++程序员可以有技巧的避免代码膨胀。
  • 在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。

Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。(省代码,省内存)

3.泛型的优点

  • 类型安全
    泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。

  • 消除强制类型转换(类型推断)
    泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。

         // 强制类型转换
         List dataList = new ArrayList();
         String dataS = (String) dataList.get(0);
         // 泛型
         List<String> strList = new ArrayList();
         String strS = strList.get(0);
  • 避免代码膨胀 (相对于C++的泛型实现而言)(Java泛型的类型擦除机制)

  • 潜在的性能收益(有点晕,不太明白)
    泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

4.泛型的两个重要概念

  • 类型推断(Type Inference)

          static <T> T pick(T a1, T a2) {  
                 return a2;  
          }  

    静态方法pick()在三个地方使用了泛型,分别限定了两个输入参数的类型与返回类型。调用该方法的代码如下:

        // 通过输入值,推断返回值
        Integer ret = pick(new Integer(1), new Integer(2));  

    两个输入参数为不同的类型,应该返回什么:java编译器就会根据这两个参数类型来推断,尽量使返回类型为最明确的一种。
    示例:”d”, new ArrayList() 两个参数, String与ArrayList都实现了同样的接口——Serializable

  • 类型擦除(Type Erasure)

    先看一个编译错误

    Cannot perform instanceof check against parameterized type Set<Integer>. Use the form Set<?> instead since further generic type information will be erased at runtime
    
    erased at runtime 这个是啥意思呢? 就是JDK在引入泛型时候,为了兼容1.5以前的版本,Set<Integer>其实是跟Set一个类型的,也就是说Set<Integer>Set<String>在运行时是一种类型,会被消除类型,只是在编译时候的强类型检查.

    也就是说在编译之后泛型会被擦除,变为1.5之前没有泛型的状态,T会被Object替代(所以泛型不能使用基本数据类型),Map集合的类型限制泛型会被擦除。

    那么泛型仅仅在编译器起数据检查作用么?答案是:No,Java 1.5引入泛型的同时还引入了接口Type,甚至可以在运行期通过反射获取泛型的信息,Type系列的接口用法会在下篇进行总结归纳。

5.泛型的注意小知识

  • 泛型类的继承
    Integer和Double都是Number的子类,但是Box与Box并不是Box的子类,不存在继承关系。Box与Box的共同父类是Object。
  • 泛型不能用基本类型表示
    上面提过,必须是Object及其子类
  • 不可实例化类型参数
    代码中还未清楚某次运行期的具体对象,故无法直接创建。
    但是运行期可以通过某些反射,先拿类型,再进行对象创建。
  • 不能在静态字段上使用泛型
    静态变量是所有实例共享的,泛型是用来限制某个对象的类型,限制某方法的类型调用
  • 不能对带有参数化类型的类使用cast或instanceof方法

        public static<E> void rtti(List<E> list) {  
            if (list instanceof ArrayList<Integer>){  // 编译错误  
                // ...  
            }  
        }  

    因为运行期list类型必然仅仅是List,而List instanceof ArrayList无意义,且逻辑不对

  • 不能创建带有参数化类型的数组(数组是不支持泛型的)

  • 不能创建、捕获泛型异常
    泛型运行期是被擦除的,数据是保存在父类的各种Type中的,此时与泛型无关了
  • 不能重载经过类型擦除后形参转化为相同原始类型的方法

        public class Example {  
            public void print(Set<String> strSet){ }  //编译错误  
            public void print(Set<Integer> intSet) { }  //编译错误  
        }  

    很好理解,泛型擦除了么,此时两方法相同,编译器无法区分

6.泛型的应用

  • 集合、类上的类型检查
  • 通过类型推断机制,让方法的使用者不再需要强转
  • 通过泛型标记,甚至可以在运行期获取,并实例化相应类型(比如:Gson)
  • 泛型运行期的使用见下篇(Type)

8.参考文章

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐