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

你们好,我是二哥呀!html

今天我来给你们讲一下,Java 不能实现真正泛型的缘由是什么?java

本文已同步至 GitHub 《教妹学 Java》专栏,风趣幽默,通俗易懂,对 Java 初学者亲切友善(目前已更新 49 篇),内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心知识点,欢迎 star。
GitHub 开源地址:github.com/itwanger/jm…
在线阅读地址:itwanger.gitee.io/jmx-java/#/git

简单来回顾一下类型擦除,看下面这段代码。程序员

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> listArrayList<Date> list 是两种不一样的类型,由于 String 和 Date 是不一样的类。github

但因为类型擦除的缘由,以上代码是不会编译经过的——编译器会提示一个错误:编程

'method(ArrayList)' clashes with 'method(ArrayList)'; both methods have same erasuremarkdown

也就是说,两个 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 教程地址:pizzacompiler.sourceforge.net/doc/tutoria…

这里插一下 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;
复制代码

表如今上面这段代码必须得可以编译运行。怎么办呢?

就只能搞类型擦除了!

真所谓“表面上一套,背后玩另一套”呀!

编译前进行泛型检测,ArrayList<Integer> 只能放 Integer,ArrayList<String> 只能放 String,取的时候就不用担忧类型强转出错了。

但编译后的字节码文件里,是没有泛型的,放的都是 Object。

Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考量,又存在 int、double 这种原始类型,但原始类型又没办法和 Object 兼容,因而咱们就只能写 ArrayList<Integer> 这样很占用内存空间的代码。

这恐怕也是 Java 泛型被吐槽的缘由之一了。

一个好消息是 Valhalla 项目正在努力解决这些由于泛型擦除带来的历史遗留问题。

Project Valhalla:正在进行当中的 OpenJDK 项目,计划给将来的 Java 添加改进的泛型支持。

源码地址:openjdk.java.net/projects/va…

我是二哥呀!

本文已同步至 GitHub 《教妹学 Java》专栏(目前已更新 49 篇),风趣幽默,通俗易懂,对 Java 初学者亲切友善,😘,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心知识点,欢迎 star

相关文章
相关标签/搜索