大家好,我是二哥呀!
今天我来给大家讲一下,Java 不能实现真正泛型的原因是什么?
本文已同步至 GitHub 《教妹学 Java》专栏,风趣幽默,通俗易懂,对 Java 初学者亲切友善,么么哒????,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心知识点,欢迎 star。
GitHub 开源地址:https://github.com/itwanger/jmx-java
CodeChina:https://codechina.csdn.net/qing_gee/jmx-java
简单来回顾一下类型擦除,看下面这段代码。
public class Cmower { public static void method(ArrayList<String> list) { System.out.println("Arraylist<String> list"); } public static void method(ArrayList<Date> list) { System.out.println("Arraylist<Date> list"); } }
在浅层的意识上,我们会认为 ArrayList<String> list 和 ArrayList<Date> list 是两种不同的类型,因为 String 和 Date 是不同的类。
但由于类型擦除的原因,以上代码是不会编译通过的——编译器会提示一个错误:
‘method(ArrayList)’ *es with ‘method(ArrayList)’; both methods have same erasure
也就是说,两个 method() 方法经过类型擦除后的方法签名是完全相同的,Java 是不允许这样做的。
也就是说,按照我们的假设:如果 Java 能够实现真正意义上的泛型,两个 method() 方法是可以同时存在的,就好像方法重载一样。
public class Cmower { public static void method(String list) { } public static void method(Date list) { } }
为什么 Java 不能实现真正意义上的泛型呢?背后的原因是什么?
第一,兼容性
Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修改为泛型类,需要让所有的用户重新修改源代码并且编译,这就会导致 Java 1.4 之前打下的*可能会完全覆灭。
想象一下,你的代码原来运行的好好的,就因为 JDK 的升级,导致所有的源代码都无法编译通过并且无法运行,是不是会非常痛苦?
类型擦除就完美实现了兼容性,Java 1.5 之后的类可以使用泛型,而 Java 1.4 之前没有使用泛型的类也可以保留,并且不用做任何修改就能在新版本的 Java 虚拟机上运行。
老用户不受影响,新用户可以*地选择使用泛型,可谓一举两得。
第二,不是“实现不了”
这部分内容参考自 R大@RednaxelaFX
Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。
Pizza 教程地址:http://pizzacompiler.sourceforge.net/doc/tutorial.html
这里插一下 Java 的版本历史,大家好有一个时间线上的观念。
1995年5月23日,Java语言诞生
1996年1月,JDK1.0 诞生
1997年2月18日,JDK1.1发布
1998年2月,JDK1.1被下载超过2,000,000次
2000年5月8日,JDK1.3发布
2000年5月29日,JDK1.4发布
2004年9月30日18:00 PM,J2SE1.5 发布
也就是说,Pizza 在 JDK 1.0 的版本上就实现了“真正意义上的”泛型,我引过来两段例子,大家一看就明白了。
首先是 StoreSomething,一个泛型类,标识符是大写字母 A 而不是我们熟悉的大写字母 T。
class StoreSomething<A> { A something; StoreSomething(A something) { this.something = something; } void set(A something) { this.something = something; } A get() { return something; } }
这个 A 呢,可以是任何合法的 Java 类型:
StoreSomething<String> a = new StoreSomething("I'm a string!"); StoreSomething<int> b = new StoreSomething(17+4); b.set(9); int i = b.get(); String s = a.get();
对吧?这就是我们想要的“真正意义上的泛型”,A 不仅仅可以是引用类型 String,还可以是基本数据类型。要知道,Java 的泛型不允许是基本数据类型,只能是包装器类型。
除此之外,Pizza 的泛型还可以直接使用 new 关键字进行声明,并且 Pizza 编译器会从构造方法的参数上推断出具体的对象类型,究竟是 String 还是 int。要知道,Java 的泛型因为类型擦除的原因,程序员是无法知道一个 ArrayList 究竟是 ArrayList<String> 还是 ArrayList<Integer> 的。
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
System.out.println(ints.getClass());
System.out.println(strs.getClass());
输出结果:
class java.util.ArrayList
class java.util.ArrayList
都是 ArrayList 而已。
那 Pizza 这种“真正意义上的泛型”为什么没有被 Java 采纳呢?这是大家都很关心的问题。
事实上,Java 的核心开发组对 Pizza 的泛型设计非常感兴趣,并且与 Pizza 的设计者 Martin 和 Phil 取得了联系,新合作了一个项目 Generic Java,争取在 Java 中添加泛型支持,但不引入 Pizza 的其他功能,比如说函数式编程。
这里再补充一点*上的资料,Martin Odersky 是一名德国计算机科学家,他和其他人一起设计了 Scala 编程语言,以及 Generic Java(还有之前的 Pizza),他实现的 Generic Java 编译器成为了 Java 编译器 javac 的基础。
站在马后炮的思维来看,Pizza 的泛型设计和函数式编程非常具有历史前瞻性。然而 Java 的核心开发组在当时似乎并不想把函数式编程引入到 Java 中。
以至于 Java 在 1.4 之前仍然是不支持泛型的,为什么 Java 1.5 的时候又突然支持泛型了呢?
当然是到了不支持不行的时候了。
没有泛型之前,我们可以这样写代码:
ArrayList list = new ArrayList();
list.add("沉默王二");
list.add(new Date());
不管是 String 类型,还是 Date 类型,都可以一股脑塞进 ArrayList 当中,这看起来似乎很方便,但取的时候就悲剧了。
String s = list.get(1);
这样取行吗?
不行。
还得加上强制转换。
String s = (String) list.get(1);
但我们知道,这行代码在运行的时候必然会出错:
Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String
这就又回到“兼容性”的问题了。
Java 语言和其他编程语言不一样,有着沉重的历史包袱,1.5 之前已经有大量的程序部署在生产环境下了,这时候如果一刀切,原来没有使用泛型的代码直接扼杀了,后果不堪想象。
Java 一直以来都强调兼容性,我认为这也是 Java 之所以能被广泛使用的主要原因之一,开发者不必担心 Java 版本升级的问题,一个在 JDK 1.4 上可以跑的代码,放在 JDK 1.5 上仍然可以跑。
这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。
但 Java 并不支持高版本 JDK 编译生成的字节码文件在低版本的 JRE(Java 运行时环境)上跑。
针对泛型,兼容性具体表现在什么地方呢?
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
ArrayList list;
list = ints;
list = strs;
1
2
3
4
5
表现在上面这段代码必须得能够编译运行。怎么办呢?
就只能搞类型擦除了!
真所谓“表面上一套,背后玩另外一套”呀!
编译前进行泛型检测,ArrayList<Integer> 只能放 Integer,ArrayList<String> 只能放 String,取的时候就不用担心类型强转出错了。
但编译后的字节码文件里,是没有泛型的,放的都是 Object。
Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考量,又存在 int、double 这种原始类型,但原始类型又没办法和 Object 兼容,于是我们就只能写 ArrayList<Integer> 这样很占用内存空间的代码。
这恐怕也是 Java 泛型被吐槽的原因之一了。
一个好消息是 Valhalla 项目正在努力解决这些因为泛型擦除带来的历史遗留问题。
Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Java 添加改进的泛型支持。
源码地址:http://openjdk.java.net/projects/valhalla/