java泛型探秘(二):泛型擦除

2024-04-27 12:08
文章标签 java 泛型 擦除 探秘

本文主要是介绍java泛型探秘(二):泛型擦除,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一. 泛型擦除是什么

二. 为什么要擦除

三. 擦除造成的限制

1. 特殊的rawType

2. 不支持原始类型

3. 不能用占位符创建实例或数组

4. 不能创建泛型数组 


一. 泛型擦除是什么

java泛型是编译期的泛型,不是运行时的泛型

       java语言是跨平台的,每个平台都有对应的JVM(java虚拟机),编写的java源码不能直接在JVM中运行,能在JVM中运行的是字节码,一般都以.class文件格式存在,java源码文件转换成.class字节码文件的过程称为编译期,编译期会对java源码严格校验,生成合格的字节码。编译过程中,编译器遇到泛型代码(泛型类、方法的定义和使用)会进行特殊处理,主要进行检查类型安全并擦除泛型信息,编译之后生成的.class字节码不包含泛型信息(实际上会保留一些泛型信息,只有在某些特殊情况下才会使用),JVM虚拟机运行时不知道泛型的存在,也不会针对泛型做特殊处理。如下代码:

/**这是一个很简单的泛型化类,拥有一个属性var,和该属性的setter和getter方法
**/
public class Generic<T> {private T var;public T getVar() {return var;}public void setVar(T var) {this.var = var;}public static void main(String[] args) {// 设置类型为IntegerGeneric<Integer> inGeneric = new Generic<Integer>();// 设置属性值inGeneric.setVar(10);// 输出值System.out.println(inGeneric.getVar().intValue()); }
}

       上面定义了泛型类Generic, 其中属性var的声明类型也是泛型修饰,并提供了var的getter和setter方法。在main方法里首先创建了Generic实例,传入类型为Integer,意味着属性var的类型是Integer,即: Generic<Integer> inGeneric = new Generic<Integer>(),并调用inGeneric.setVar(10)设置var值为10,调用inGeneric.getVar().intValue()获取var的值。上面这段示例代码虽然简单,但是在类定义、属性声明、方法入参与出参、泛型类实例创建和方法调用上都使用了,通过观察编译后的.class文件,比较能全面直观地了解泛型擦除的效果。

       使用javap -v 命令输出.class字节码内容

javap -v Generic.class

       输出结果,由于字节码内容比较多,这里只摘出部分内容:

Classfile /D:/workspace/learn-class/bin/cn/learn/classes/Generic.classLast modified 2019-4-8; size 1247 bytesMD5 checksum 917868e0a5c70d19355976a97fe8b62eCompiled from "Generic.java"
public class cn.learn.classes.Generic<T extends java.lang.Object> extends java.lang.Objectminor version: 0major version: 51flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Class              #2             // cn/learn/classes/Generic#2 = Utf8               cn/learn/classes/Generic#3 = Class              #4             // java/lang/Object#4 = Utf8               java/lang/Object#5 = Utf8               var....public T getVar();descriptor: ()Ljava/lang/Object;flags: ACC_PUBLICSignature: #22                          // ()TT;Code:stack=1, locals=1, args_size=10: aload_01: getfield      #23                 // Field var:Ljava/lang/Object;4: areturnLineNumberTable:line 8: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/learn/classes/Generic;LocalVariableTypeTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcn/learn/classes/Generic<TT;>;public void setVar(T);descriptor: (Ljava/lang/Object;)Vflags: ACC_PUBLICSignature: #27                          // (TT;)VCode:stack=2, locals=2, args_size=20: aload_01: aload_12: putfield      #23                 // Field var:Ljava/lang/Object;5: returnLineNumberTable:line 12: 0line 13: 5LocalVariableTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcn/learn/classes/Generic;0       6     1   var   Ljava/lang/Object;LocalVariableTypeTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcn/learn/classes/Generic<TT;>;0       6     1   var   TT;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10: new           #1                  // class cn/learn/classes/Generic3: dup4: invokespecial #30                 // Method "<init>":()V7: astore_18: aload_19: bipush        1011: invokestatic  #31                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;14: invokevirtual #37                 // Method setVar:(Ljava/lang/Object;)V17: getstatic     #39                 // Field java/lang/System.out:Ljava/io/PrintStream;20: aload_121: invokevirtual #45                 // Method getVar:()Ljava/lang/Object;24: checkcast     #32                 // class java/lang/Integer27: invokevirtual #47                 // Method java/lang/Integer.intValue:()I30: invokevirtual #51                 // Method java/io/PrintStream.println:(I)V33: returnLineNumberTable:line 19: 0line 22: 8line 25: 17line 26: 33LocalVariableTable:Start  Length  Slot  Name   Signature0      34     0  args   [Ljava/lang/String;8      26     1 inGeneric   Lcn/learn/classes/Generic;LocalVariableTypeTable:Start  Length  Slot  Name   Signature8      26     1 inGeneric   Lcn/learn/classes/Generic<Ljava/lang/Integer;>;
}
SourceFile: "Generic.java"
Signature: #63                          // <T:Ljava/lang/Object;>Ljava/lang/Object;

       在10-11行,类名上定义的泛型T被擦除,和普通的类名一样;在13-14行,属性var的类型T被擦除,用Object类型的全限定名java/lang/Object表示;在18-19行,getVar()返回类型T被擦除,descriptor描述符表示方法的入参和出参的实际类型,表明返回类型T被java/lang/Object替换;在36-37行,setVar(T)的入参类型T被参数,descriptor表明入参类型T被java/lang/Object替换;在63行,创建Generic实例时传入的Integer丢失,和创建普通的类一样;在70行,显示调用setVar方法时实际调用了setVar(java/lang/Object),因为实参10会自动封箱为Integer类型,所以能成功传入到setVar(java/lang/Object)方法中;在73-74行,显示getVar实际返回的参数是java/lang/Object,并且为了使用Integer的intValue()方法输出属性值,会将getVar的返回参数java/lang/Object检查并强转成Integer类型。

       通过上面的.class字节码和源码对比,可以知道在编译阶段,在类名、属性和方法上定义的泛型占位符和使用泛型类时传入的实际类型参数都会被擦除或者被java/lang/Object替换(注:泛型会擦除到定义的泛型边界,默认边界是java/lang/Object,可以用extends自定义边界),另外在一些地方编译器插入了强制类型转换,而这些开发人员是无感知的。当JVM加载.class文件时,由于泛型信息都被擦除了,JVM感受不到泛型的存在。其他编程语言如C++,泛型模板在源码和运行阶段都是存在的,相比较而言,java的泛型更像一种语法糖,只提供泛型编码能力,真正运行时,泛型并不存在。

二. 为什么要擦除

       泛型在java5正式发布,更早之前,在java1只推出一年后,Scala之父Martin Odersky就用java实现了Pizza项目,Pizza有三大特性,其中之一就是实现了"真正的java泛型"(泛型信息在运行阶段依然存在)。然后java核心开发者Gilad Bracha 和David Stoutamire 邀请 Martin Odersky为java实现泛型功能,如果这个时候他们能赶在java下个版本发布泛型,"真正的泛型"功能可能会被实现,事实上,java泛型不仅没能在下个版本发布,反而推迟了6年(在没有泛型的版本中,数组承担了部分泛型责任,java核心开发者认为数组中的方法应该是通用的,这也是为什么数组是协变的原因)。

       等真正确定要在java5版本中添加泛型特性时,java已经经历了好几个版本,如果要对java集合类等核心类实现泛化,有两种方案,第一种方案是重新实现一套完整的泛型集合类,对之前未泛化的集合类完全抛弃;第二种方案是直接将原来的集合类泛化,兼容未泛化代码和字节码。第一种方案的优点是有效地隔离了泛型和未泛型,甩掉了历史包袱,只需专注于怎样更好地实现泛型类;缺点是新增了大量新泛型集合类的api,需要java程序员大量的学习成本,旧代码改造成泛型很困难。第二种方案优点是新旧集合类平滑过渡,可以逐步对项目内之前未泛化的代码进行泛化;缺点是需要谨慎处理泛化和未泛化之间的兼容性。

       最终第二种方案胜出,由于有大量未泛型化的java源码和字节码编译文件存在,java核心开发者认为java应该具有"完全的向后兼容性"(源码和字节码都要兼容),比如要对java集合库实现泛型化就要兼容之前的非泛型化的集合库(在java5之前,ArrayList和LinkedList等是未泛化集合类,在java5中,ArrayList泛化为ArrayList<E>,LinkedList泛化为LinkedList<E>),要求对原来的非泛型集合库的代码和字节码都能兼容,才有了rawType特殊写法和编译期擦除的泛型实现方式。Martin Odersky在关于Scala的访谈中,吐槽了java擦除泛型,看了之后能对目前的java泛型实现机制有更好的理解,访谈地址:https://www.artima.com/scalazine/articles/origins_of_scala.html

三. 擦除造成的限制


1. 特殊的rawType

       在使用泛型类时,声明泛型类对象或者创建泛型类实例,一般都会用真实类型替换泛型占位符,如果没有替换,也能正常编译运行 ,这是因为java泛型是完全的向后兼容(源码和字节码都兼容),java5已泛型化的类在java5之前的版本中是未被泛型化的,所以包含了非泛型类的代码也是可以用java编译器编译的,并将未泛型化类称为泛型化类的rawType,比如List是List<E>的rawType(原生类型)。虽然java保留了rawType,但在编写java泛型代码时,尽量避免使用rawType,否则容易发生类型不安全。如下代码,三种方式都是可以编译,但是提倡使用第一种:

// 第一种:正常的泛型类声明和创建实例
List<String> strList = new ArrayList<String>();// 第二种:声明使用rawType
List rawList1 = new ArrayList<String>();// 第三种:创建实例使用rawType
List<String> rawList2 = new ArrayList();


2. 不支持原始类型

       目前编译器擦除泛型信息时,擦除到边界(默认是Object),边界类型要求是类,不支持原始类型(byte、short、int、long、boolean、char、float、double)。原始类型和类的数据结构不同,如果要实现擦除之后的泛型类同时支持原生类型和类,实现较为困难,java核心开发者考虑到了实现成本,只能暂时放弃了对原始类型的泛型化(Project Valhalla是正在进行中的OpenJDK项目,计划给未来的Java添加改进的泛型支持以及原始类型支持)。注:java具有封箱功能,在传入原始类型的值时,java会自动封箱为对应的包装类,如下代码:

// 编译失败, 不允许传入原始类型
List<int> intList = new ArrayList<int>();// 编译成功, 允许传入原始类型的值
List<Integer> integerList = new ArrayList<Integer>();
integerList.add(10); // 参数值10自动封箱为 Integer(10)

3. 不能用占位符创建实例或数组

       java编译后会擦除泛型信息,占位符被边界类型(默认是Object)代替,所以不能用new 关键字创建占位符T的实例或者数组,只能用占位符声明对象,如下代码:

public class Generic<T> {// 可以用占位符T声明var和arrVarprivate T var;private T[] arrVar;// 编译失败, 不能同占位符T创建实例和数组public Generic(){var = new T(); // 编译失败arrVar = new T[10]; // // 编译失败}}

4. 不能创建泛型数组 

       数组支持协变的,所以在编译时无法进行类型安全验证,只能在运行时验证类型安全。而泛型信息在编译器就会被擦除,在运行阶段无法验证类型安全,如果java支持泛型数组的创建,会导致该数组在编译和运行阶段都无法进行类型检查。如下代码: 

// 用泛型类型声明数组对象
List<String>[] arr = null;// 假设编译成功, 创建泛型数组
arr = new ArrayList<String>[10];// 声明object数组, 并赋值arr
Object[] objArr = arr;// 数组中的第一个元素赋值
objArr[0] = new ArrayList<Integer>();// 取出arr第一个元素并遍历
List<String> firstOfArr = arr[0];for(String s : firstOfArr){...
}    

       上面代码模拟了如果java支持泛型数组,即上面代码中的 new ArrayList<String>[10]  假设编译成功,又因为数组是支持协变的,所以可以将new ArrayList<String>[10] 赋值给声明为List<String>[]的arr。接下来将arr赋值给声明为Object[] 的objArr ,然后为objArr[0] 赋值ArrayList<Integer>实例,因为不违反类型安全,所以这段代码能成功编译。在编译阶段,这段代码的泛型信息会被擦除,arr = new ArrayList<String>[10] 擦除之后实际变成了arr = new ArrayList[10](即arr是原生类型ArrayList的数组),objArr[0] = new ArrayList<Integer>() 实际变成了objArr[0] = new ArrayList()(即原生类型ArrayList),那么这段代码在运行阶段是可以成功运行的,但是当获取arr第一个元素并遍历时,因为实际元素是Integer类型,遍历用string类型时,会发生类型转换错误,导致发生了堆污染。

 👉👉👉 自己搭建的租房网站:全网租房助手,m.kuairent.com,每天新增 500+房源

这篇关于java泛型探秘(二):泛型擦除的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/940468

相关文章

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

java中long的一些常见用法

《java中long的一些常见用法》在Java中,long是一种基本数据类型,用于表示长整型数值,接下来通过本文给大家介绍java中long的一些常见用法,感兴趣的朋友一起看看吧... 在Java中,long是一种基本数据类型,用于表示长整型数值。它的取值范围比int更大,从-922337203685477

java Long 与long之间的转换流程

《javaLong与long之间的转换流程》Long类提供了一些方法,用于在long和其他数据类型(如String)之间进行转换,本文将详细介绍如何在Java中实现Long和long之间的转换,感... 目录概述流程步骤1:将long转换为Long对象步骤2:将Longhttp://www.cppcns.c

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte

SpringBoot服务获取Pod当前IP的两种方案

《SpringBoot服务获取Pod当前IP的两种方案》在Kubernetes集群中,SpringBoot服务获取Pod当前IP的方案主要有两种,通过环境变量注入或通过Java代码动态获取网络接口IP... 目录方案一:通过 Kubernetes Downward API 注入环境变量原理步骤方案二:通过

Springboot整合Redis主从实践

《Springboot整合Redis主从实践》:本文主要介绍Springboot整合Redis主从的实例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言原配置现配置测试LettuceConnectionFactory.setShareNativeConnect

Java中Map.Entry()含义及方法使用代码

《Java中Map.Entry()含义及方法使用代码》:本文主要介绍Java中Map.Entry()含义及方法使用的相关资料,Map.Entry是Java中Map的静态内部接口,用于表示键值对,其... 目录前言 Map.Entry作用核心方法常见使用场景1. 遍历 Map 的所有键值对2. 直接修改 Ma