用了这么多年的 Java 泛型,你对它到底有多了解?

做为一个 Java 程序员,平常编程早就离不开泛型。泛型自从 JDK1.5 引进以后,真的很是提升生产力。一个简单的泛型 T,寥寥几行代码, 就可让咱们在使用过程当中动态替换成任何想要的类型,不再用实现繁琐的类型转换方法。html

虽然咱们天天都在用,可是还有不少同窗可能并不了解其中的实现原理。今天这篇咱们从如下几点聊聊 Java 泛型:java

  • Java 泛型实现方式
  • 类型擦除带来的缺陷
  • Java 泛型发展史

用了这么多年的 Java 泛型,你对它到底有多了解?

Java 泛型实现方式

Java 采用类型擦除(Type erasure generics)的方式实现泛型。用大白话讲就是这个泛型只存在源码中,编译器将源码编译成字节码之时,就会把泛型『擦除』,因此字节码中并不存在泛型。程序员

对于下面这段代码,编译以后,咱们使用 javap -s class 查看字节码。编程

用了这么多年的 Java 泛型,你对它到底有多了解?

用了这么多年的 Java 泛型,你对它到底有多了解?
观察setParam 部分的字节码,从 descriptor 能够看到,泛型 T 已被擦除,最终替换成了 Object数组

安全

ps:并非每个泛型参数被擦除类型后都会变成 Object 类,若是泛型类型为 T extends String 这种方式,最终泛型擦除以后将会变成 String。ide

同理getParam 方法,泛型返回值也被替换成了 Object函数式编程

为了保证 String param = genericType.getParam(); 代码的正确性,编译器还得在这里插入类型转换。函数

除此以外,编译器还会对泛型安全性防护,若是咱们往 ArrayList<String> 添加 Integer,程序编译期间就会报错。学习

最终类型擦除后的代码等同与以下:

用了这么多年的 Java 泛型,你对它到底有多了解?

类型擦除带来的缺陷

做为对比,咱们再来简单聊下 C# 泛型的实现方式。

C#泛型实现方式为「具现化式泛型(Reifiable generics)」,不熟悉的 C#\小伙伴能够不用纠结**具现化**技术概念,我也不了解这些特性--!

简单点来说,C#实现的泛型,不管是在程序源码,仍是在编译以后的,甚至是运行期间都是切实存在的。

相对比与 C# 泛型,Java 泛型看起来就像是个「」泛型。Java 泛型只存在程序源码中,编译以后就被擦除,这种缺陷相应的会带来一些问题。

不支持基本数据类型

泛型参数被擦除以后,强制变成了 Object 类型。这么作对于引用类型来讲没有什么问题,毕竟 Object 是全部类型的父类型。可是对于 int/long 等八个基本数据类型说,这就难办了。由于 Java 没办法作到int/longObject 的强制转换。

若是要实现这种转换,须要进行一系列改造,改动难度还不小。因此当时 Java 给出一个简单粗暴的解决方案:既然没办法作到转换,那就索性不支持原始类型泛型了。

若是须要使用,那就规定使用相关包装类的泛型,好比 ArrayList<Integer>。另外为了开发人员方便,顺便增长了原生数据类型的自动拆箱/装箱的特性。

正是这种「偷懒」的作法,致使如今咱们没办法使用原始类型泛型,又要忍受包装类装箱/拆箱带来的开销,从而又带来运行效率的问题。

运行效率

上面字节码例子咱们已经看到,泛型擦除以后类型将会变成 Object。当泛型出如今方法输入位置的时候,因为 Java 是能够向上转型的,这里并不须要强制类型转换,因此没有什么问题。

可是当泛型参数出如今方法的输出位置(返回值)的时候,调用该方法的地方就须要进行向下转换,将 Object 强制转换成所需类型,因此编译器会插入一句 checkcast 字节码。

除了这个,上面咱们还说到原始基本数据类型,编译器还需帮助咱们进行装箱/拆箱。

因此对于下面这段代码来讲:

List<Integer> list = new ArrayList<Integer>();
list.add(66); // 1
int num = list.get(0); // 2

对于①处,编译器要作就是增长基本类型的装箱便可。可是对于第二步来讲,编译器首先须要将 Object 强制转换成 Integer,接着编译器还须要进行拆箱。

类型擦除以后,上面代码等同于:

List list = new ArrayList();
list.add(Integer.valueOf(66));
int num = ((Integer) list.get(0)).intValue();

若是上面泛型代码在 C# 实现,就不会有这么多额外步骤。因此 Java 这种类型擦除式泛型实现方式不管使用效果与运行效率,仍是全面落后于 C# 的具现化式泛型。

运行期间没法获取泛型实际类型

因为编译以后,泛型就被擦除,因此在代码运行期间,Java 虚拟机没法获取泛型的实际类型。

下面这段代码,从源码上两个 List 看起来是不一样类型的集合,可是通过泛型擦除以后,集合都变为 ArrayList。因此 if语句中代码将会被执行。

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // 泛型擦除,两个 List 类型是同样的
    System.out.println("6666");
}

这样代码看起来就有点反直觉,这对新手来讲不是很友好。

另外还会给咱们在实际使用中带来一些限制,好比说咱们没办法直接实现如下代码:

用了这么多年的 Java 泛型,你对它到底有多了解?

最后再举个例子,好比说咱们须要实现一个泛型 List 转换成数组的方法,咱们就没办法直接从 List 去获取泛型实际类型,因此咱们不得不额外再传入一个 Class 类型,指定数组的类型:

public static <E> E[] convert(List<E> list, Class<E> componentType) {
    E[] array = (E[]) Array.newInstance(componentType, list.size());
    ....
}

从上面的例子咱们能够看到,Java 采用类型擦除式实现泛型,缺陷不少。那为何 Java 不采用 C# 的那种泛型实现方式?或者说采用一种更好实现方式?

这个问题等咱们了解 Java 泛型机制的历史,以及当时 Java 语言的现状,咱们才能切身体会到当时 Java 采用这种泛型实现方式的缘由。

Java 泛型历史背景

Java 泛型最先是在 JDK5 的时候才被引入,可是泛型思想最先来自来自 C++ 模板(template)。1996 年 Martin Odersky(Scala 语言缔造者) 在刚发布的 Java 的基础上扩展了泛型、函数式编程等功能,造成一门新的语言-「Pizza」。

后来,Java 核心开发团队对 Pizza 的泛型设计深感兴趣,与 Martin 合做,一块儿合做开发的一个新的项目「Generic Java」。这个项目的目的是为了给 Java 增长泛型支持,可是不引入函数式编程等功能。最终成功在 Java5 中正式引入泛型支持。

用了这么多年的 Java 泛型,你对它到底有多了解?

泛型移植过程,一开始并非朝着类型擦除的方向前进,事实 Pizza 中泛型更加相似于 C# 中的泛型。

可是因为 Java 自身特性,自带严格的约束,让 Martin 在Generic Java 开发过程当中,不得不放弃了 Pizza 中泛型设计。

这个特性就是,Java 须要作到严格的向后兼容性。也就是说一个在 JDK1.2 编译出来 Class 文件,不只能在 JDK 1.2 能正常运行,还得必须保证在后续 JDK,好比 JDK12 中也能保证正常的运行。

这种特性是明确写入 Java 语言规范的,这是一个对 Java 使用者的一个严肃承诺。

这里强调一下,这里的向后兼容性指的是二进制兼容性,并非源码兼容性。也不保证高版本的 Class 文件可以运行在低版本的 JDK 上。

如今困难点在于,Java 1.4.2 以前都没有支持泛型,而 Java5 以后忽然要支持泛型,还要让 JDK1.4 以前编译的程序能在新版本中正常运行,这就意味着之前没有的限制,就不能忽然增长。

举个例子:

ArrayList arrayList=new ArrayList();
arrayList.add("6666");
arrayList.add(Integer.valueOf(666));

没有泛型以前, List 集合是能够存储不一样类型的数据,那么引入泛型以后,这段代码必须的能正确运行。

为了保证这些旧的 Clas 文件能在 Java5 以后正常运行,设计者基本有两条路:

  1. 须要泛型化的容器(主要是容器类型),之前有的保持不变,平行增长一套新的泛型化的版本。
  2. 直接把已有的类型原地泛型化,不增长任何新的已有类型的泛型版本。

若是 Java 采用第一条路实现方式,那么如今咱们可能就会有两套集合类型。以 ArrayList 为例,一套为普通的 java.util.ArrayList,一套可能为 java.util.generic.ArrayList&lt;T&gt;

采用这种方案以后,若是开发中须要使用泛型特性,那么直接使用新的类型。另外旧的代码不改动,也能够直接运行在新版本 JDK 中。

这套方案看起来没什么问题,实际上C# 就是采用这套方案。可是为何 Java 却没有使用这套方案那?

这是由于当时 C# 才发布两年,历史代码并很少,若是旧代码须要使用泛型特性,改造起来也很快。可是 Java 不同,当时 Java 已经发布十年了,已经有不少程序已经运行部署在生产环境,能够想象历史代码很是多。

若是这些应用在新版本 Java 须要使用泛型,那就须要作大量源码改动,能够想象这个开发工做量。

另外 Java 5 以前,其实咱们就已经有了两套集合容器,一套为 Vector/Hashtable 等容器,一套为 ArrayList/ HashMap。这两套容器的存在,其实已经引来一些不便,对于新接触的 Java 的开发人员来讲,还得学习这二者的区别。

若是此时为了泛型再引入新类型,那么就会有四套容器同时并存。想一想这个画面,一个新接触开发人员,面对四套容器,彻底不知道如何下手选择。如何 Java 真的这么实现了,想必会有更多人吐槽 Java。

因此 Java 选择第二条路,采用类型擦除,只须要改动 Javac 编译器,不须要改动字节码,不须要改动虚拟机,也保证了以前历史没有泛型的代码还能够在新的 JDK 中运行。

可是第二条路,并不表明必定须要使用类型擦除实现,若是有足够时间好好设计,也许会有更好的方案。

当年留下的技术债,如今只能靠 Valhalla 项目来还了。这个项目从2014 年开始立项,本来计划在 JDK10 中解决现有语言的各类缺陷。可是结果咱们也知道了,如今都 JDK14 了,还只是完成少部分目标,并无解决核心目标,可见这个改动的难度啊。

总结

本文咱们先从 Java 泛型底层实现方式开始聊起,接着举了几个例子,让你们了解如今泛型实现方式存在一些缺陷。

而后咱们带入 Java 泛型历史背景,站在 Java 核心开发者的角度,才能了解 Java 泛型这么现实无奈缘由。

最后做为 Java 开发者,让咱们对于如今 Java 一些不足,少些抱怨,多一些理解吧。相信以后 Java 核心开发人员确定会解决泛型现有的缺陷,让咱们拭目以待。

帮助资料

  1. https://www.zhihu.com/question/38940308
  2. https://www.zhihu.com/question/28665443
  3. https://hllvm-group.iteye.com/group/topic/25910
  4. http://blog.zhaojie.me/2010/05/why-java-sucks-and-csharp-rocks-4-generics.html
  5. http://blog.zhaojie.me/2010/04/why-java-sucks-and-csharp-rocks-2-primitive-types-and-object-orientation.html
  6. https://en.wikipedia.org/wiki/Generics_in_Java
  7. https://www.zhihu.com/question/34621277/answer/59440954
  8. https://www.artima.com/scalazine/articles/origins_of_scala.html

最后(求关注,求点赞,求转发)

本文是在看了『深刻 Java虚拟机(第三版)』以后,知道 Java 泛型这些故事,才有本篇文章。

首先感谢一下机械工业出版社的小哥哥的赠书。

刚开始知道『深刻 Java虚拟机(第三版)』发布的时候,原本觉得只是对第二版稍微补充而已。等收到这本书的时候,才发现本身错了。两本书放在一块儿,彻底就不是一个量级的。

ps:盗取一张 Why 神的图

用了这么多年的 Java 泛型,你对它到底有多了解?

第三本在第二版的基础增长大量补充,也解决了第二版留下一些没解释的问题。因此没买的同窗,推荐直接购买第三版,买他,买他,买他~

两个版本的具体区别,你们能够看下 Why 神的的文章,这篇文章还被本书的做者打赏过哦。

我告诉你这书的第 3 版到底值不值得买?

我是楼下小黑哥,一个还未秃的程序猿,咱们下周三见~

相关文章
相关标签/搜索