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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定