Retrofit完全解析(二):泛型(Generic)
Retrofit源码阅中,发现自带晕眩光环的Type(简直无CD),而谈到Type绕不过泛型,借这个机会好好捋捋。1.泛型的由来引入版本: Java平台在JDK 5中引入了一个重要的特性 —— 泛型(generics)(又被称作参数化类型 parameterized type),。泛型引入的目:为了解决容器(数据结构)的类型安全性,使得编译器在编译时就能发现明显的类型错误,从而避免运行时的转型错误
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.参考文章
更多推荐
所有评论(0)