大家好,我是二哥呀!

今天我来给大家讲一下,Java 不能实现真正泛型的原因是什么?

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

GitHub 开源地址:https://github.com/itwanger/jmx-java
码云开源地址:https://gitee.com/itwanger/jmx-java
CodeChina:https://codechina.csdn.net/qing_gee/jmx-java
在线阅读地址:https://itwanger.gitee.io/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> listArrayList<Date> list 是两种不同的类型,因为 String 和 Date 是不同的类。

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

'method(ArrayList)' clashes 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;

表现在上面这段代码必须得能够编译运行。怎么办呢?

就只能搞类型擦除了!

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

编译前进行泛型检测,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/


我是二哥呀!

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

剖根问底:Java 不能实现真正泛型的原因是什么?的更多相关文章

  1. 深度学习剖根问底: Adam优化算法的由来

    在调整模型更新权重和偏差参数的方式时,你是否考虑过哪种优化算法能使模型产生更好且更快的效果?应该用梯度下降,随机梯度下降,还是Adam方法? 这篇文章介绍了不同优化算法之间的主要区别,以及如何选择最佳 ...

  2. 【问底】徐汉彬:PHP7和HHVM的性能之争 (真是学到了很多)

    来源:http://www.csdn.net/article/2014-12-25/2823234 作者:徐汉彬 摘要:近日,PHP7和HHVM的性能之争成为了一个讨论热点,但毫无疑问,它们都在提升P ...

  3. 面试官,不要再问我“Java 垃圾收集器”了

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...

  4. 面试官,不要再问我“Java 垃圾收集器”了(转载)

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在 面试过程中这个深度的问题涉及的比 ...

  5. 【Todo】CSDN的《问底》系列-学习

    看到CSDN的这个系列<问底>,看各篇文章的题目感觉不错.好好学习下: http://www.csdn.net/tag/%E9%97%AE%E5%BA%95/news

  6. 【问底】王帅:深入PHP内核(一)——弱类型变量原理探究

    来源:CSDN    http://www.csdn.net/article/2014-09-15/2821685-exploring-of-the-php 作者:王帅 摘要:PHP作为一门简单而强大 ...

  7. 求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…

    GitHub 4.1k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.1k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.1k Star 的 ...

  8. 面试官,不要再问我“Java GC垃圾回收机制”了

    Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解> ...

  9. 面试官,不要再问我“Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

随机推荐

  1. 08:jQuery(01)

    今日内容概要 jQuery(封装了js的前端框架(模块)) 很容易与DOM操作混淆 jQuery """ jQuery内部封装了原生的js代码(还额外添加了很多的功能) ...

  2. 《吃透MQ系列》之扒开Kafka的神秘面纱

    大家好,这是<吃透 MQ 系列>的第二弹,有些珊珊来迟,后台被好几个读者催更了,实属抱歉! 这篇文章拖更了好几周,起初的想法是:围绕每一个具体的消息中间件,不仅要写透,而且要控制好篇幅,写 ...

  3. Mysql 面试题(一网打尽,收藏版)

    文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...

  4. 【模板】Linux下输出文件的对比

    命令格式: diff+[参数]+[文件1或目录1]+[文件2或目录2] 命令参数: 指定要显示多少行的文本.此参数必须与-c或-u参数一并使用. -a或--text diff预设只会逐行比较文本文件. ...

  5. [Docker核心之容器、数据库文件的导入导出、容器镜像的导入导出]

    [Docker核心之容器.数据库文件的导入导出] 使用 Docker 容器 在 Docker 中,真正对外提供服务的还是容器,容器是对外提供服务的实例,容器的本质是进程. 运行一个容器 docker ...

  6. 解析 Nebula Graph 子图设计及实践

    本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践. 前言 在先前的 Query Engine 源码解析中,我们介绍了 2.0 ...

  7. Arduino参考手册-函数和变量及电路图

    标题: Arduino参考手册-函数和变量及电路图 作者: 梦幻之心星 sky-seeker@qq.com 标签: [#Arduino,#参考手册,#函数,#变量] 目录: [Arduino] 日期: ...

  8. sql数据库新建作业,新建步骤时报错从 IClassFactory 为 CLSID 为 {AA40D1D6-CAEF-4A56-B9BB-D0D3DC976BA2} 的 COM 组件创建实例失败,原因是出现以下错误: c001f011。 (Microsoft.SqlServer.ManagedDTS)

    简单粗暴的重启sql数据库 其他网上找的方法 32位操作系统: 打开运行(命令提示符), 一.输入 cd c:\windows\system32 进入到c:\windows\system32路径中 二 ...

  9. Centos7安装配置jenkins(Tomcat)

    Centos7安装配置jenkins(Tomcat) 一.准备工作 1.1 安装JDK1.8 具体安装过程不在赘述. 1.2 下载jenkins的war包 jenkins官网下载地址:https:// ...

  10. 创建Akamai cdn api授权

    注:通过Akamai Cli purge和通过Akamai API进行刷新之前,都要事先创建类似于如下的刷新的凭据,这两种刷新方式所创建的凭据是相同的. 目的:创建Akamai CDN API授权以便 ...