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实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2