惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
F
Full Disclosure
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Register - Security
The Register - Security
G
Google Developers Blog
C
Check Point Blog
GbyAI
GbyAI
A
About on SuperTechFans
V
Vulnerabilities – Threatpost
T
The Blog of Author Tim Ferriss
T
Tor Project blog
AWS News Blog
AWS News Blog
Cyberwarzone
Cyberwarzone
C
CERT Recently Published Vulnerability Notes
MongoDB | Blog
MongoDB | Blog
Latest news
Latest news
aimingoo的专栏
aimingoo的专栏
U
Unit 42
Y
Y Combinator Blog
P
Privacy International News Feed
Cisco Talos Blog
Cisco Talos Blog
S
Securelist
S
Schneier on Security
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
C
Cisco Blogs
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
P
Privacy & Cybersecurity Law Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
罗磊的独立博客
Cloudbric
Cloudbric
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Security Blog
Microsoft Security Blog

青空之蓝

[青空之蓝-2023] - 色彩 | 青空之蓝 [青空之蓝-2022] - 平静 | 青空之蓝 [青空之蓝-2021] - 远望 | 青空之蓝 浅谈垃圾回收 | 青空之蓝 浅谈单点登录 | 青空之蓝 使用 Kotlin 编写 Spring 测试 | 青空之蓝 设计模式系列文章 | 青空之蓝 从零实现一个 Java 微框架 - IoC | 青空之蓝 从零实现一个 Java 微框架 - 前言 | 青空之蓝 浅谈 JVM:类加载 | 青空之蓝 浅谈 IO | 青空之蓝 浅谈并发:synchronized & ReentrantLock | 青空之蓝 浅谈并发:CAS & AQS | 青空之蓝 浅谈并发:ThreadLocal | 青空之蓝 浅谈并发:三大特性 | 青空之蓝 浅谈组合注解 & 注解别名 | 青空之蓝 [青空之蓝-2020]-迷茫 | 青空之蓝 Java 系列文章 | 青空之蓝 HTTP 系列文章 | 青空之蓝 浅谈 EatWhatYouKill | 青空之蓝 浅谈可扩展线程池 | 青空之蓝 聊聊写框架 | 青空之蓝 聊聊现状-[2020-09] | 青空之蓝 浅谈并发:锁 | 青空之蓝 浅谈并发:基础 | 青空之蓝 浅谈缓存 | 青空之蓝 无须定义类,Spring 快速注入 Json 参数 | 青空之蓝 浅谈 Proxy 和 Aop | 青空之蓝 从零实现一个 PHP 微框架 - 初始化请求 | 青空之蓝 为 Vue3 添加一个简单的 Store | 青空之蓝 从零实现一个 PHP 微框架 - 服务提供者 | 青空之蓝 WSL2 踩坑记录 | 青空之蓝 浅谈浏览器Event Loop [更新] | 青空之蓝 从零实现一个 PHP 微框架 - Bootstrap 启动加载 | 青空之蓝 从零实现一个 PHP 微框架 - IoC 容器 | 青空之蓝 从零实现一个 PHP 微框架 - PSR & Composer | 青空之蓝 从零实现一个 PHP 微框架 - 前言 | 青空之蓝 MVVM 简单实现 | 青空之蓝 浅谈 DI 和 IoC | 青空之蓝 中间件实现 [PHP] | 青空之蓝 告别 Windows 终端的难看难用,打造好用的 PowerShell | 青空之蓝 VSCode Java输出中文乱码问题解决[更新] | 青空之蓝 浅谈浏览器渲染 | 青空之蓝 Vue-Cli@2 项目迁移日志 | 青空之蓝 Laragon & Scoop 集成踩坑记录 | 青空之蓝 「一行代码」优雅管理 Windows 软件 | 青空之蓝 [青空之蓝-2019]-年度总结 | 青空之蓝 为Vue添加简单的Store | 青空之蓝 为React添加简单的Store | 青空之蓝 为Vuex添加同步Action | 青空之蓝 浅谈B+树 | 青空之蓝 浅谈跳表 | 青空之蓝 浅谈数据库索引 | 青空之蓝 MySQL事务隔离 | 青空之蓝 算法复杂度分析(1) | 青空之蓝 一年来的经验总结 | 青空之蓝 Acrylic - VSCode Extension | 青空之蓝 ace编辑器设置惯性滚动 | 青空之蓝 Java二叉树实现 | 青空之蓝 Java图实现 | 青空之蓝 为apt方式安装的nginx重新编译增加WebDAV | 青空之蓝 XK-Editor - 一个支持富文本和Markdown的编辑器 | 青空之蓝 JS生成列表树 | 青空之蓝 Laravel生成目录树 | 青空之蓝 XK-Note - 集各种神奇功能的云笔记 | 青空之蓝 PHP GD生成验证码 | 青空之蓝 PHP GD图片处理[转换格式-水印-缩略图] | 青空之蓝 Origami - 简洁轻快的WordPress主题 | 青空之蓝 为WordPress启用WorkBox | 青空之蓝 [青空之蓝-2018]-年度总结 | 青空之蓝 VSCode Java手动导入jar和源码包 | 青空之蓝 Windows IP变化自动发送邮件 | 青空之蓝 C链表实现重制版 | 青空之蓝 C 结构体的定义和使用 | 青空之蓝 图的搜索(遍历) - BFS & DFS | 青空之蓝 Java链表实现 | 青空之蓝 C 快速排序 | 青空之蓝 C 插入排序 | 青空之蓝 C 归并排序 | 青空之蓝 C语言链表实现 | 青空之蓝 VSCode配置Java调试环境[Windows] | 青空之蓝 C 选择排序 | 青空之蓝 C 冒泡排序 | 青空之蓝 VSCode配置PHP调试环境[Windows] | 青空之蓝 VSCode配置C/C++ GDB调试环境[Windows] | 青空之蓝 WordPress友情链接模板 | 青空之蓝 Intel Optane 傲腾内存体验 | 青空之蓝 Mysql双机热备实战 | 青空之蓝 博客一年记录 | 青空之蓝 为WordPress启用Service Worker | 青空之蓝 Bing每日一图API | 青空之蓝 iframe延迟加载 | 青空之蓝 写在2018年高考前 | 青空之蓝 The Fox主题汉化分享 | 青空之蓝 [青空之蓝-2017]-崭新 | 青空之蓝 本博客评论规则 | 青空之蓝 世界,您好! | 青空之蓝
浅谈泛型擦除 | 青空之蓝
Otstar Lin · 2021-07-01 · via 青空之蓝

浅谈泛型擦除

前言

泛型是 Java 5 引入的一个新特性,现在的高级语言基本也都支持泛型了。泛型本质上是将类型作为参数,以提供类型检查和避免不必要的类型转换。具体关于类型的本篇文章就不再说明了,具体可以自行查找。

什么是泛型擦除?

在 Java 中的泛型,常常被称为伪泛型。之所以这么说是因为 Java 的泛型在经过编译时会将实际的类型信息擦除掉。在字节码中就不存在泛型的信息了,JVM 运行的时候自然而然的就没有泛型信息了。

真泛型与伪泛型:

  • 真泛型:即泛型在运行期间是真实存在的,运行时可以取得泛型对应的实际类型,如 this.getComponent<ExampleComponent>() 方法可以取得 ExampleComponent 类型的实例。
  • 伪泛型:即泛型在运行期间不可见,仅在编译时进行类型检查,如 this.getComponent<ExampleComponent>(ExampleComponent.class) 方法,必须传入 ExampleComponent.class 才能取得对应类型的实例,因为泛型定义 <ExampleComponent> 在运行期间无法取得,只能通过另外传入类型的方式来实现相同的功能。

为什么 Java 要进行泛型擦除?

在说明原因的时候我们需要了解真泛型和伪泛型的实现机制。

实现思路

编程语言引入真泛型的方式一般是使用代码膨胀的方式,举个例子来说当我们有一个 Component<T> 的类,那么代码经过编译后会生成如 Component@T 的类,其中多出来的 @T 分别为标记和占位符。当我们使用的时候如 new Component<String>(),此时编译器则会将原始类中的 T 占位符更改为 String 类,同时生成一个新的类,如 Component@String 类,此时实际类型就被保留下来了。当我们使用的时候就可以同普通类一样使用,如:

if (obj instanceof T) {}
new T();
new T[1];
if (obj instanceof T) {}
new T();
new T[1];

伪泛型的处理方式则很简单,还是举个例子来说吧,当 Component<T> 编译后会将其中的 T 占位符去除,变成 Component 类,当我们使用的时候如 new Component<String>() 那么经过编译后,会将 String 类型标注去除,变成 Component 相当于 Component<Object>

使用真泛型带来的问题

Java 是在 Java 5 后才引入的泛型支持,为了支持真泛型需要修改 JVM 源代码,加入泛型支持,同时为了兼容 Java 5 以前的程序,不能改动以前的旧版本类,而是另外新增一套支持泛型的新版本类,如 java.util.ArrayListjava.util.generic.ArrayList<T>

这样的实现看似很简单,只需要引入一套泛型,当需要使用泛型的时候就使用泛型包下的类,似乎还更灵活一点?但是我们忽略了一个问题,当项目中某个依赖库使用了泛型重新生成了字节码文件,此时这个项目为了兼容就必须也同时升级到 Java 5,否则会抛出 UnsupportedClassVersionError 错误。而 Java 5 之前的生态已经非常完善,此时如果为了引入泛型,就不得不让许多库都修改代码,显然这是不合理的。

为什么 Spring 还能进行泛型注入?

实现思路

我们知道 Spring 可以通过集合元素的泛型来注入对应的依赖,如下面的代码,在容器启动后,Spring 会将容器内对应类型的依赖注入到响应的变量中。

class Demo1ApplicationTests {
    @Autowired
    List<ApplicationContext> contexts;

    @Autowired
    Map<String, ApplicationContext> contextMap;

    @Test
    void contextLoads() {
        log.info("ApplicationContext: {}", contexts);
        log.info("ApplicationContext: {}", contextMap);
    }
}
class Demo1ApplicationTests {
    @Autowired
    List<ApplicationContext> contexts;

    @Autowired
    Map<String, ApplicationContext> contextMap;

    @Test
    void contextLoads() {
        log.info("ApplicationContext: {}", contexts);
        log.info("ApplicationContext: {}", contextMap);
    }
}

既然 Java 会进行泛型擦除,那么这两个字段在 JVM 中实际上是 List<Object>Map<Object, Object>,理应是无法注入的,那么 Spring 又是通过何种手段取得这本不存在的信息呢?

实际上 Java 虽然将泛型信息擦除了,但是为了能够让虚拟机解析、反射等各种场景正确获取到参数类型,JCP 组织修改了虚拟机规范,引入了 SignatureLocalVariableTypeTable。这样即使泛型被擦除了,在一些特定的场景下还是能取得泛型信息。我们就以上面的代码为例,通过 jclasslib 查看字节码信息(偷懒不用 javap):

可以看到特有信息里出现了 ApplicationContext 的身影,这就是保留下来的泛型信息。在 Java 中,以下场景都会提供签名:

  • 具有通用或者具有参数化类型的超类或者超接口的类。
  • 方法中的通用或者参数化类型的返回值或者入参,以及方法的 throw 子句中的类型变量。
  • 任何类型、类型变量、或者参数化类型的字段、形式参数或者局部变量。

取得泛型信息

既然知道 Java 会将泛型信息保留到 Signature,那么我们就可以取得对应的泛型信息了,如下:

class Demo1ApplicationTests {
    @Autowired
    List<ApplicationContext> contexts;

    @Test
    void contextLoads() throws NoSuchMethodException, NoSuchFieldException {
        // 具有泛型的超类
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) TestClass.class.getGenericSuperclass()
                ).getActualTypeArguments()
            )
        );
        // 返回值
        final Method method =
            this.getClass().getDeclaredMethod("testMethod", List.class);
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) method.getGenericReturnType()
                ).getActualTypeArguments()
            )
        );
        // 参数
        log.info(
            "Type: {}",
            Arrays
                .stream(method.getGenericParameterTypes())
                .map(type -> (ParameterizedType) type)
                .map(ParameterizedType::getActualTypeArguments)
                .collect(Collectors.toList())
        );
        // 字段
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) this.getClass()
                        .getDeclaredField("contexts")
                        .getGenericType()
                ).getActualTypeArguments()
            )
        );
    }

    List<ApplicationContext> testMethod(List<ApplicationContext> contexts) {
        return contexts;
    }

    public static class TestClass extends ArrayList<ApplicationContext> {}
}
class Demo1ApplicationTests {
    @Autowired
    List<ApplicationContext> contexts;

    @Test
    void contextLoads() throws NoSuchMethodException, NoSuchFieldException {
        // 具有泛型的超类
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) TestClass.class.getGenericSuperclass()
                ).getActualTypeArguments()
            )
        );
        // 返回值
        final Method method =
            this.getClass().getDeclaredMethod("testMethod", List.class);
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) method.getGenericReturnType()
                ).getActualTypeArguments()
            )
        );
        // 参数
        log.info(
            "Type: {}",
            Arrays
                .stream(method.getGenericParameterTypes())
                .map(type -> (ParameterizedType) type)
                .map(ParameterizedType::getActualTypeArguments)
                .collect(Collectors.toList())
        );
        // 字段
        log.info(
            "Type: {}",
            Arrays.toString(
                (
                    (ParameterizedType) this.getClass()
                        .getDeclaredField("contexts")
                        .getGenericType()
                ).getActualTypeArguments()
            )
        );
    }

    List<ApplicationContext> testMethod(List<ApplicationContext> contexts) {
        return contexts;
    }

    public static class TestClass extends ArrayList<ApplicationContext> {}
}

文中就只列举了几种常见的场景,不过需要注意有些泛型信息虽然有保留但是使用 Java 反射无法获取,如局部变量,Java 没有对应局部变量的反射机制,自然也无法取得对应的泛型信息,需要额外借助 ASM 等字节码工具类来进行读取。

擦除方式

在 Java 中,我们为了限制泛型的使用会使用一些修饰符来定义泛型的上下限,如以下几种定义:

<?>           // 无限制通配符
<T>           // 无限制通配符
<? extends E> // extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E>   // super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
<T extends Staff & Passenger> // 合并限制
<?>           // 无限制通配符
<T>           // 无限制通配符
<? extends E> // extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E>   // super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
<T extends Staff & Passenger> // 合并限制

从上面的几种修饰方式我们可以看出,泛型擦除被分成了有限制泛型擦除和无限制泛型擦除两种。

无限制泛型擦除

无限制泛型擦除,当类或方法定义中的类型参数没有任何限制时,即形如 <T><?> 的类型参数都属于无限制泛型擦除。在进行无限制泛型擦除时,Java 编译器会将这些泛型信息都替换为 Object

如以下的测试代码,其编译后的签名为 <<T:Ljava/lang/Object;>(TT;)V>

public static <T> void object(final T object) {} // <<T:Ljava/lang/Object;>(TT;)V>
public static <T> void object(final T object) {} // <<T:Ljava/lang/Object;>(TT;)V>

有限制泛型擦除

有限制泛型擦除,当类或方法参数等类型参数存在限制(上下界)时,即形如 <T extends Number><? extends Number><? super Number> 的类型参数都属于有限制泛型擦除。在进行有限制泛型擦除的时候 Java 编译器会将这些泛型信息进行变更。其中 extends 修饰的会被替换成具体的类型,如 <T extends Number> 会替换为 Numbersuper 修饰的会被替换为 Object

如以下的测试代码,其编译后的签名为 <<T:Ljava/lang/Number;>(TT;)V>

public static <T extends Number> void number(final T number) {} // <<T:Ljava/lang/Number;>(TT;)V>
public static <T extends Number> void number(final T number) {} // <<T:Ljava/lang/Number;>(TT;)V>

合并泛型擦除

合并泛型擦除也属于有限制泛型擦除,不过由于其泛型定义具有多个类型,其签名也存在多个类型信息,如以下的测试代码,其编译后包含了两个类型:<<T::Lorg/springframework/context/ApplicationContext;:Lorg/springframework/context/annotation/AnnotationConfigRegistry;>(TT;)V>

public static <T extends ApplicationContext & AnnotationConfigRegistry> void merge(final T merge) {} // <<T::Lorg/springframework/context/ApplicationContext;:Lorg/springframework/context/annotation/AnnotationConfigRegistry;>(TT;)V>
public static <T extends ApplicationContext & AnnotationConfigRegistry> void merge(final T merge) {} // <<T::Lorg/springframework/context/ApplicationContext;:Lorg/springframework/context/annotation/AnnotationConfigRegistry;>(TT;)V>

需要注意一点,虽然签名中保留了多个泛型信息,但是在进行反射取得方法的时候需要使用第一个类型来获取:

this.getClass().getDeclaredMethod("merge", ApplicationContext.class);
this.getClass().getDeclaredMethod("merge", ApplicationContext.class);

泛型检查

由于泛型是在语法层面上支持的也就是说编译器上支持的,所以自然而然就要有泛型的检查,否则泛型就没有存在的意义了。

比如以下的代码:

public static void main(String[] args) {
  List<String> list = new ArrayList<String>();
  list.add("123");
  list.add(123); //编译错误
}
public static void main(String[] args) {
  List<String> list = new ArrayList<String>();
  list.add("123");
  list.add(123); //编译错误
}

由于我们定义 List 的元素类型是 String,所以当添加的类型不为 String 的时候,Java 编译器就会检测出来,并抛出编译错误。即使在运行时会进行 泛型擦除,List 可以存储任何 继承 自 Object 的类型,不过运行时是处于编译后,在编译期间就会对泛型的使用进行检查。

泛型检查是针对泛型的,如果原始使用的话就不会报编译错误,这是因为如果不写泛型,那么 Java 会默认泛型为 Object 。如下:

public static void main(String[] args) {
  List list = new ArrayList();
  list.add("123");
  list.add(123); //编译通过
}
public static void main(String[] args) {
  List list = new ArrayList();
  list.add("123");
  list.add(123); //编译通过
}

还有一种常见的泛型检查是泛型间类型的转换,如下:

List<String> list1 = new ArrayList<Object>(); //编译错误
List<Object> list2 = new ArrayList<String>(); //编译错误
List<String> list1 = new ArrayList<Object>(); //编译错误
List<Object> list2 = new ArrayList<String>(); //编译错误

第一种转换报错的原因是因为 Object 类型无法转换为 String,如果 List 中存储了非 String 类型的对象,当我们获取的时候,Java 会默认认为 List 中所有对象都是 String,而此时如果获取它,就会抛出 ClassCastException 异常。因此 Java 编译器不允许此种情况的发生。

第二种虽然可以正常工作,但是如果这样转换泛型也就失去了意义,所以 Java 编译器也不允许这种情况的发生。

结语

这篇文章从 5 月份就开始计划写了,一直咕到了今天 🤣。

最近应该都会写一些深入一点的文章(类似这篇),框架系列就一直没写了,等过几天看看吧(逃。

引用

浅谈泛型擦除

https://blog.ixk.me/post/talking-about-type-erasure
  • 许可协议

    BY-NC-SA

  • 本文作者

    Otstar Lin

  • 发布于

    2021/07/01

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

浅谈垃圾回收浅谈单点登录