剖根问底:Java 不能实现真正泛型的原因是什么?

大家好,我是二哥呀!


今天我来给大家讲一下,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/


上一篇:Flask 教程 第二十二章:后台作业


下一篇:memory_limit session.cache_expire ecshop初始化注释说明