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

文章目录

  • 概述
  • Java泛型实现方式
  • 类型擦除带来的缺陷
    • 不支持基本数据类型
    • 运行效率
    • 运行期间无法获取泛型实际类型
  • Java泛型历史背景
  • 本文小结

概述

泛型虽然我们每天都在用,但是还有很多同学可能并不了解其中的实现原理。今天我们从以下几点聊聊 Java 泛型:

  1. Java 泛型实现方式
  2. 类型擦除带来的缺陷
  3. Java 泛型发展史


Java泛型实现方式

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

对于下面这段代码,编译之后,我们使用 javap -v class 查看字节码。

源代码

package cn.wideth.util.proxy;public class GenericType<T> {private  T  type;public  void  setParam(T type){this.type = type;}public T  getParam(){return  type;}public static void main(String[] args) {GenericType<String>  genericType = new GenericType<>();genericType.setParam("hello");String param = genericType.getParam();System.out.println(param);}
}

字节码

Last modified 2021-5-15; size 1324 bytesMD5 checksum 30fe62d45a62b8e948b2efe82d9612c9Compiled from "GenericType.java"
public class cn.wideth.util.proxy.GenericType<T extends java.lang.Object> extends java.lang.Objectminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #11.#43        // java/lang/Object."<init>":()V#2 = Fieldref           #3.#44         // cn/wideth/util/proxy/GenericType.type:Ljava/lang/Object;#3 = Class              #45            // cn/wideth/util/proxy/GenericType#4 = Methodref          #3.#43         // cn/wideth/util/proxy/GenericType."<init>":()V#5 = String             #46            // hello#6 = Methodref          #3.#47         // cn/wideth/util/proxy/GenericType.setParam:(Ljava/lang/Object;)V#7 = Methodref          #3.#48         // cn/wideth/util/proxy/GenericType.getParam:()Ljava/lang/Object;#8 = Class              #49            // java/lang/String#9 = Fieldref           #50.#51        // java/lang/System.out:Ljava/io/PrintStream;#10 = Methodref          #52.#53        // java/io/PrintStream.println:(Ljava/lang/String;)V#11 = Class              #54            // java/lang/Object#12 = Utf8               type#13 = Utf8               Ljava/lang/Object;#14 = Utf8               Signature#15 = Utf8               TT;#16 = Utf8               <init>#17 = Utf8               ()V#18 = Utf8               Code#19 = Utf8               LineNumberTable#20 = Utf8               LocalVariableTable#21 = Utf8               this#22 = Utf8               Lcn/wideth/util/proxy/GenericType;#23 = Utf8               LocalVariableTypeTable#24 = Utf8               Lcn/wideth/util/proxy/GenericType<TT;>;#25 = Utf8               setParam#26 = Utf8               (Ljava/lang/Object;)V#27 = Utf8               MethodParameters#28 = Utf8               (TT;)V#29 = Utf8               getParam#30 = Utf8               ()Ljava/lang/Object;#31 = Utf8               ()TT;#32 = Utf8               main#33 = Utf8               ([Ljava/lang/String;)V#34 = Utf8               args#35 = Utf8               [Ljava/lang/String;#36 = Utf8               genericType#37 = Utf8               param#38 = Utf8               Ljava/lang/String;#39 = Utf8               Lcn/wideth/util/proxy/GenericType<Ljava/lang/String;>;#40 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;#41 = Utf8               SourceFile#42 = Utf8               GenericType.java#43 = NameAndType        #16:#17        // "<init>":()V#44 = NameAndType        #12:#13        // type:Ljava/lang/Object;#45 = Utf8               cn/wideth/util/proxy/GenericType#46 = Utf8               hello#47 = NameAndType        #25:#26        // setParam:(Ljava/lang/Object;)V#48 = NameAndType        #29:#30        // getParam:()Ljava/lang/Object;#49 = Utf8               java/lang/String#50 = Class              #55            // java/lang/System#51 = NameAndType        #56:#57        // out:Ljava/io/PrintStream;#52 = Class              #58            // java/io/PrintStream#53 = NameAndType        #59:#60        // println:(Ljava/lang/String;)V#54 = Utf8               java/lang/Object#55 = Utf8               java/lang/System#56 = Utf8               out#57 = Utf8               Ljava/io/PrintStream;#58 = Utf8               java/io/PrintStream#59 = Utf8               println#60 = Utf8               (Ljava/lang/String;)V
{public cn.wideth.util.proxy.GenericType();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/wideth/util/proxy/GenericType;LocalVariableTypeTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/wideth/util/proxy/GenericType<TT;>;//请看这里public void setParam(T);descriptor: (Ljava/lang/Object;)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: aload_12: putfield      #2                  // Field type:Ljava/lang/Object;5: returnLineNumberTable:line 8: 0line 9: 5LocalVariableTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcn/wideth/util/proxy/GenericType;0       6     1  type   Ljava/lang/Object;LocalVariableTypeTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcn/wideth/util/proxy/GenericType<TT;>;0       6     1  type   TT;MethodParameters:Name                           FlagstypeSignature: #28                          // (TT;)Vpublic T getParam();descriptor: ()Ljava/lang/Object;flags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: getfield      #2                  // Field type:Ljava/lang/Object;4: areturnLineNumberTable:line 12: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/wideth/util/proxy/GenericType;LocalVariableTypeTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/wideth/util/proxy/GenericType<TT;>;Signature: #31                          // ()TT;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new           #3                  // class cn/wideth/util/proxy/GenericType3: dup4: invokespecial #4                  // Method "<init>":()V7: astore_18: aload_19: ldc           #5                  // String hello11: invokevirtual #6                  // Method setParam:(Ljava/lang/Object;)V14: aload_115: invokevirtual #7                  // Method getParam:()Ljava/lang/Object;18: checkcast     #8                  // class java/lang/String21: astore_222: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;25: aload_226: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V29: returnLineNumberTable:line 17: 0line 18: 8line 19: 14line 20: 22line 21: 29LocalVariableTable:Start  Length  Slot  Name   Signature0      30     0  args   [Ljava/lang/String;8      22     1 genericType   Lcn/wideth/util/proxy/GenericType;22       8     2 param   Ljava/lang/String;LocalVariableTypeTable:Start  Length  Slot  Name   Signature8      22     1 genericType   Lcn/wideth/util/proxy/GenericType<Ljava/lang/String;>;MethodParameters:Name                           Flagsargs
}
Signature: #40                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "GenericType.java"

观察setParam部分的字节码,从 descriptor 可以看到,泛型 T 已被擦除,最终替换成了 Object

并不是每一个泛型参数被擦除类型后都会变成 Object 类,如果泛型类型为 T extends String 这种方式,最终泛型擦除之后将会变成 String。

同理getParam 方法,泛型返回值也被替换成了 Object。

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

除此之外,编译器还会对泛型安全性防御,如果我们往 ArrayList 添加 Integer,程序编译期间就会报错。

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

package cn.wideth.util.proxy;public class GenericType{private  Object  type;public GenericType(){}public  void  setParam(Object type){this.type = type;}public Object  getParam(){return  this.type;}public static void main(String[] args) {GenericType genericType = new GenericType();genericType.setParam("hello");String param = (String)genericType.getParam();System.out.println(param);}
}

类型擦除带来的缺陷

不支持基本数据类型

泛型参数被擦除之后,强制变成了 Object 类型。这么做对于引用类型来说没有什么问题,毕竟 Object 是所有类型的父类型。但是对于 int/long 等八个基本数据类型说,这就难办了。因为 Java 没办法做到int/long 与 Object 的强制转换。

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

如果需要使用,那就规定使用相关包装类的泛型,比如 ArrayList。另外为了开发人员方便,顺便增加了原生数据类型的自动拆箱/装箱的特性。

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


运行效率

上面字节码例子我们已经看到,泛型擦除之后类型将会变成 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();

所以 Java 这种类型擦除式泛型实现方式无论使用效果与运行效率,都是比较差的。


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

由于编译之后,泛型就被擦除,所以在代码运行期间,Java 虚拟机无法获取泛型的实际类型。

下面这段代码,从源码上两个 List 看起来是不同类型的集合,但是经过泛型擦除之后,集合都变为 ArrayList。所以 if语句中代码将会被执行。

代码

package cn.wideth.util.proxy;import java.util.ArrayList;public class GenericTypeTest {public static void main(String[] args) {ArrayList<Integer> li = new ArrayList<>();ArrayList<Float> lf = new ArrayList<>();// 泛型擦除,两个 List 类型是一样的if (li.getClass() == lf.getClass()) {System.out.println("hello,GenericType");}}
}

程序结果


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


Java泛型历史背景

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

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


泛型移植过程,一开始并不是朝着类型擦除的方向前进,事实 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。

采用这种方案之后,如果开发中需要使用泛型特性,那么直接使用新的类型。另外旧的代码不改动,也可以直接运行在新版本 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 核心开发人员肯定会解决泛型现有的缺陷,让我们拭目以待。

深入理解Java泛型相关推荐

  1. 十分钟理解Java泛型擦除

    泛型信息只存在于代码编译阶段,但是在java的运行期(已经生成字节码文件后)与泛型相关的信息会被擦除掉,专业术语叫做类型擦除. 今天我们来讲解泛型中另一个重要知识点--泛型擦除! 泛型擦除概念 泛型信 ...

  2. 深入理解 Java 泛型

    首先提个问题: Java 泛型的作用是什么?泛型擦除是什么?泛型一般用在什么场景? 如果这个问题你答不上来,那这篇文章可能就对你有些价值. 读完本文你将了解到: 什么是泛型 为什么引入泛型 泛型的使用 ...

  3. 深入理解 Java 泛型擦除机制

    我们都知道 Java 中的泛型可以在编译期对类型检查,避免类型强制转化带来的问题,保证代码的健壮性.不同语言对泛型的支持也不一样,Java 中的泛型类型在编译期会擦除,下面一个例子可以证明这一点: p ...

  4. java null转换jason_Java进阶知识,轻松理解Java泛型

    在学习泛型之前我们先回顾下Java的数据类型以及涉及到的一些概念. Java数据类型 Java的两大数据类型分为基础类型和引用类型.基本类型的数值不是对象,不能调用对象的toString().hash ...

  5. 浅显理解Java泛型的super和extends

    目录 概念简单理解 代码样例解读 关于List<? super T> add方面 返回值方面 关于List<? extendsT> add方面 返回值方面 总结 概念简单理解 ...

  6. 深入Java泛型(三):泛型的上下边界

    泛型的命名规范 为了更好地去理解泛型,我们也需要去理解java泛型的命名规范. 为了与java关键字区别开来,java泛型参数只是使用一个大写字母来定义.各种常用泛型参数的意义如下: E - Elem ...

  7. java泛型程序设计——反射和泛型

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 反射和泛型 的相关知识: [1]反射和泛型相关 1.1)现在, Cl ...

  8. java泛型程序设计——无限定通配符+通配符捕获

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 无限定通配符+通配符捕获 的相关知识: [1]无限定通配符相关 1. ...

  9. java泛型程序设计——通配符类型+通配符的超类型限定

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 通配符类型+通配符的超类型限定 的知识: [1]通配符类型相关 1. ...

最新文章

  1. 2018年人工智能和机器学习路在何方?听听美国公司怎么做
  2. 微服务网关 Kong 快速上手攻略
  3. CentOS7安装MySQL并设置远程登录
  4. 结构体成员管理AVClass AVOption之2AVOption,设置选项值
  5. 材料科学中的数据挖掘:晶体图神经网络解读与代码解析
  6. boost::geometry::dissolver用法的测试程序
  7. 安装 openSUSE Leap 42.1 之后要做的 8 件事
  8. jquery sleep函数
  9. Spring注解依赖注入的三种方式的优缺点以及优先选择
  10. php后门 佛像,35张活的再久,也未必见过的照片,图2是佛像通过CT扫描后内部照...
  11. 为什么我会放弃 Webpack 而选择 Vite
  12. 自创算法实现Reporting Service中多值判定
  13. Docker Swarm学习教程
  14. 忆往昔,惜光阴似箭,不堪回首
  15. CAD中级的考证费用是多少?
  16. 计算机考研408-2010
  17. 墨刀 - 简单 易用的APP原型设计工具
  18. 计算机体系结构 第三章 CPU性能公式 CPI相关计算
  19. 修改CodeRunner快捷键
  20. 【异常处理】解决Windows下access denied for user ‘root‘@‘localhost‘(using password:YES)的mysql启动问题

热门文章

  1. 设计模式学习笔记——命令模式
  2. HOWTO: Create and submit your first Linux kernel patch using GIT
  3. C#中的异步调用剖析
  4. 【Unity Shader】(六) ------ 复杂的光照(上)
  5. 基于springMVC的页面跳转、转发、重定向等
  6. Flex布局新旧混合写法详解
  7. 如何为***选择合适的动态密码双因素认证方案
  8. postgresql数据类型之数组类型
  9. 下班理财超过上班赚钱
  10. html5 worker的使用场景