Java数据结构 (泛型第二节) 泛型擦除机制/泛型的限制/上界下界

news/2024/9/20 0:35:41 标签: java, 数据结构, 开发语言

书接上回:Java数据结构 (泛型第一节) 为什么要有泛型/泛型语法/泛型方法-CSDN博客

访问作者Github:
https://github.com/Joeysoda/Github_java/blob/main/20240908%E6%B3%9B%E5%9E%8B/src/%E6%B3%9B%E5%9E%8B.java

目录

1. 为什么要有擦除机制?

2. 类型擦除的工作原理

2.1 泛型类的类型擦除

2.2 泛型方法的类型擦除

3. 泛型的上界与下界

3.1 指定上界的泛型

4. 类型擦除带来的限制

4.1 无法创建泛型数组

4.2 无法使用 instanceof 检查泛型类型

4.3 无法创建泛型实例


1. 为什么要有擦除机制?

Java 在 1.5 版本引入了泛型,但 Java 语言的设计理念是保证向下兼容。这意味着即使引入了泛型,Java 程序仍然要能够与不使用泛型的老版本代码(比如 Java 1.4 及更早的代码)兼容。为了实现这一点,Java 的泛型设计为在编译时执行类型检查,而在运行时完全去除泛型信息。这种机制被称为类型擦除

如果没有类型擦除机制,使用泛型的 Java 程序在运行时将需要处理更多的类型信息,这可能会导致与现有 Java 代码的兼容性问题,并且会增加运行时的复杂性。

2. 类型擦除的工作原理

泛型的类型参数 T 在编译后会被替换为其上界(Upper Bound),如果没有指定上界,则默认为 Object。编译器会在编译时插入必要的类型转换代码,以确保类型安全。

2.1 泛型类的类型擦除

示例代码:

java">public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        box.setValue("Hello");
        String value = box.getValue();
        System.out.println(value);  // 输出: Hello
    }
}

编译时类型擦除后的代码:

java">public class Box {
    private Object value;  // T 被替换为 Object

    public void setValue(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box box = new Box();
        box.setValue("Hello");
        String value = (String) box.getValue();  // 编译器自动插入类型转换
        System.out.println(value);  // 输出: Hello
    }
}

解释

  • 在编译后的代码中,泛型类型 T 被擦除为 Object,这就是类型擦除的工作方式。
  • 在运行时,Box 类实际上只保存了 Object 类型,而不是 String 类型。
  • 编译器自动插入了类型转换 (String) box.getValue(),以保证类型安全。
2.2 泛型方法的类型擦除

泛型方法示例:

java">public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

编译时类型擦除后的代码:

java">public static void printArray(Object[] array) {  // T 被擦除为 Object
    for (Object element : array) {
        System.out.println(element);
    }
}

解释

  • 在泛型方法的编译后,类型参数 T 被替换为了 Object,这意味着在运行时,方法 printArray 接受的参数类型就是 Object[],而不再区分具体的泛型类型。

3. 泛型的上界与下界

当泛型类或方法定义了类型边界时,类型擦除后泛型类型会被替换为其上界。如果没有指定上界,则替换为 Object

3.1 指定上界的泛型

代码示例:

java">public class NumberBox<T extends Number> {  // T 的上界是 Number
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

编译后的代码:

java">public class NumberBox {
    private Number value;  // T 被替换为 Number

    public void setValue(Number value) {
        this.value = value;
    }

    public Number getValue() {
        return value;
    }
}

解释

  • 在编译后,泛型类型 T 被替换为了 Number,因为在定义时我们指定了 T extends Number,即 T 的上界是 Number
  • 这意味着编译后的 NumberBox 类可以接受任何 Number 类型的子类对象(如 Integer, Double),但所有泛型信息在运行时都被擦除了。

4. 类型擦除带来的限制

由于泛型的类型擦除机制,Java 中的泛型有一些限制。以下是类型擦除带来的一些常见问题:

4.1 无法创建泛型数组

由于类型擦除,泛型类型在运行时被擦除为 Object,因此 Java 不允许直接创建泛型数组。因为在运行时无法知道数组元素的具体类型。

java">T[] array = new T[10];  // 错误:不能创建泛型数组

解决方案: 可以通过强制类型转换来创建泛型数组,使用 Object[] 来存储泛型类型。

java">T[] array = (T[]) new Object[10];  // 通过类型转换创建泛型数组

但这种方式会导致编译器发出未经检查的转换警告,因为无法在运行时确定数组的实际类型。

4.2 无法使用 instanceof 检查泛型类型

由于泛型在运行时被擦除为 Object,所以不能使用 instanceof 检查泛型类型。

错误示例

java">if (obj instanceof T) {  // 错误,无法进行泛型类型检查
    // ...
}

解决方案: 可以通过检查擦除后的类型来替代 instanceof,比如使用泛型的上界。

java">if (obj instanceof Number) {  // T extends Number,可以检查 Number 类型
    // ...
}
4.3 无法创建泛型实例

由于泛型在运行时类型被擦除为 Object 或其上界,因此不能在类中直接创建泛型类型的实例。

错误示例

java">T obj = new T();  // 错误:无法实例化泛型类型

解决方案: 可以通过传递一个类的引用(Class<T> 对象)来间接创建泛型对象。

示例

java">class Box<T> {
    private T value;

    public Box(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        // 使用传递的 Class 对象创建泛型实例
        this.value = clazz.newInstance();
    }
}

面试题目:

1. 为什么 T[] ts = new T[5]; 是不对的?为什么不能直接创建泛型数组?

问题:
既然在编译时泛型 T 会被替换为 Object,为什么 T[] ts = new T[5]; 不能写成类似于 Object[] ts = new Object[5];

答案解析:

泛型的类型擦除机制并不能直接与数组创建的机制兼容,因为数组在 Java 中具有 "协变性"(covariant)。这意味着,如果 Integer[]Object[] 的子类型,那么可以将 Integer[] 赋值给 Object[] 类型的变量。这会导致潜在的类型安全问题,尤其是在数组的运行时类型检查过程中,泛型类型擦除与数组的运行时行为产生了冲突。

解决方案
可以通过创建 Object[] 数组并进行类型转换的方式间接创建泛型数组,但这会发出未经检查的类型转换警告

  1. 数组的运行时类型检查:数组在 Java 中保留其类型信息,并且能够在运行时进行类型检查。如果尝试在运行时向 Object[] 数组中插入不兼容的元素,Java 会抛出 ArrayStoreException,因为数组知道自己是存储哪种类型的对象的。

    例如:

    java">Object[] objects = new Integer[5];
    objects[0] = "Hello";  // 运行时会抛出 ArrayStoreException
    

  2. 泛型的类型擦除:Java 的泛型在编译时擦除类型信息,所以如果我们能够创建 T[] ts = new T[5];,编译器无法确保数组的类型安全,因为擦除后 T 被替换为了 Object。这意味着我们无法在运行时对数组的元素类型进行检查,可能会导致数组元素的类型与数组声明的类型不匹配。

具体原因总结

  • 数组需要在运行时保留类型信息,而泛型在运行时会被擦除为 Object,因此创建泛型数组无法保证类型安全。
  • 如果允许 T[] 创建,会导致运行时无法检查数组的实际类型,可能会导致 ArrayStoreException

解决方案
可以通过创建 Object[] 数组并进行类型转换的方式间接创建泛型数组,但这会发出未经检查的类型转换警告

java">@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[5];  // 警告:未经检查的类型转换

2. 类型擦除一定是把 T 变成 Object 吗?

问题:
在类型擦除过程中,泛型 T 是否总是被擦除为 Object

答案解析:

不一定。泛型 T 在类型擦除过程中,会根据类型参数的上界来进行替换。如果没有指定上界,那么 T 会被替换为 Object;但如果有上界,T 会被替换为它的上界类型。

  1. 无上界的类型擦除:当 T 没有上界时,T 会被擦除为 Object

java">class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

        经过类型擦除后:

java">class Box {
    private Object value;  // T 被擦除为 Object

    public void setValue(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }
}

有上界的类型擦除:如果 T 有上界,例如 T extends Number,那么在类型擦除时,T 会被替换为 Number

代码示例:

java">class Box<T extends Number> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

经过类型擦除后:

java">class Box {
    private Number value;  // T 被擦除为 Number

    public void setValue(Number value) {
        this.value = value;
    }

    public Number getValue() {
        return value;
    }
}

总结

  • 无上界泛型 T:擦除为 Object
  • 有上界泛型 T:擦除为其上界类型。

http://www.niftyadmin.cn/n/5666387.html

相关文章

C++ const引用(这里不讲返回,只是讲引用,拷贝构造篇章会有更详细的讲解)

个人主页&#xff1a;Jason_from_China-CSDN博客 所属栏目&#xff1a;C系统性学习_Jason_from_China的博客-CSDN博客 所属栏目&#xff1a;C知识点的补充_Jason_from_China的博客-CSDN博客 使用规则 引用常量对象&#xff1a;可以引用一个常量对象&#xff0c;但必须使用常量引…

关于实时数仓的几点技术分享

一、实时数仓建设背景 业务需求的变化&#xff1a;随着互联网和移动互联网的快速发展&#xff0c;企业的业务需求变得越来越复杂和多样化&#xff0c;对数据处理的速度和质量要求也越来越高。传统的T1数据处理模式已经无法满足企业的需求&#xff0c;实时数据处理成为了一种必…

点云深度学习系列:Sam2Point——基于提示的点云分割

文章&#xff1a;SAM2POINT:Segment Any 3D as Videos in Zero-shot and Promptable Manners 代码&#xff1a;https://github.com/ZiyuGuo99/SAM2Point Demo&#xff1a;https://huggingface.co/spaces/ZiyuG/SAM2Point 1&#xff09;摘要 文章介绍了SAM2POINT&#xff0c;这是…

【海康威视面经】

海康威视面经 Java基础java常用集合 及其优缺点ArrayListVectorLinkedList Jvm调优监控发现问题工具分析问题 &#xff1a;性能调优GC频繁 出现内存泄漏 内存溢出CPU飙升 Synchronized和Volatile的比较反射线程池和new thread利弊高并发 集群 分布式 负载均衡 MySQL调优基础优化…

SSL/TSL 总结

参考&#xff1a;https://blog.csdn.net/qq153471503/article/details/109524764 &#xff08;一&#xff09;生成CA证书 1、创建CA证书私钥 openssl genrsa -aes256 -out ca.key 2048 &#xff08;密码&#xff1a;ca1234567890&#xff09; 2、请求证书 openssl req -new -s…

计算机网络:概述 --- 体系结构

目录 一. 体系结构总览 1.1 OSI七层协议体系结构 1.2 TCP/IP四层(或五层)模型结构 二. 数据传输过程 2.1 同网段传输 2.2 跨网段传输 三. 体系结构相关概念 3.1 实体 3.2 协议 3.3 服务 这里我们专门来讲一下计算机网络中的体系结构。其实我们之前…

从零开始手搓Transformer#Datawhale组队学习Task1#

从零开始手搓Transformer 目录 缩放点积注意力DotProductAttention 多头注意力Multi-Head Attention 位置编码Position Encoder 前馈神经网络FFN 残差连接和层归一化&#xff08;Add&Norm&#xff09; 编码器Encoder 解码器Decoder 编码器-解码器Encoder-Decoder …

从零开始学PostgreSQL (十四):高级功能

目录 1. 简介 2. 视图 3. 外键 4. 事务 5. 窗口函数 6. 继承 7. 结论 简介 PostgreSQL是一个强大且开源的关系型数据库管理系统&#xff0c;以其稳定性、功能丰富性和对SQL标准的广泛支持而闻名。它不仅提供了传统的关系型数据库功能&#xff0c;如事务处理、外键约束和视图&am…