Java复习之范型相关 类型擦除

目录

是什么意思

上下届限定符

[List和 List 的区别](#List和 List 的区别)

[List和 List 的区别](#List和 List 的区别)

类型擦除

泛型与编译的关系

一、先明确:泛型的"编译时核心作用"

[二、List和 List 在编译时的差异](#二、List和 List 在编译时的差异)

[1. 类型检查规则:编译器如何判断"操作是否合法"?](#1. 类型检查规则:编译器如何判断“操作是否合法”?)

[(1)List的编译检查:明确允许"所有 Object 及其子类"](#(1)List的编译检查:明确允许“所有 Object 及其子类”)

[(2)List 的编译检查:"未知类型"导致的严格限制](#(2)List 的编译检查:“未知类型”导致的严格限制)

[2. 类型擦除后的差异:编译后泛型信息如何消失?](#2. 类型擦除后的差异:编译后泛型信息如何消失?)

三、总结:编译时差异的核心

四、擦除前后的变化

[1. 的擦除与逻辑体现](#1. 的擦除与逻辑体现)

[2. 的擦除与逻辑体现](#2. 的擦除与逻辑体现)

五、总结:关键结论


<T>是什么意思

这是一种类型参数

用于定义通用类型 这是一种约定 本质上我们可以指定具体的类型来代替 T

定义 Box 类

复制代码
// 泛型类,T 是类型参数
class Box<T> {
    private T content; // 用 T 定义变量类型

    public void setContent(T content) { // 用 T 定义方法参数类型
        this.content = content;
    }

    public T getContent() { // 用 T 定义返回值类型
        return content;
    }
}

验证

复制代码
// T 被替换为 String,盒子只能存字符串
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello"); 
String str = stringBox.getContent(); // 无需强制类型转换

// T 被替换为 Integer,盒子只能存整数
Box<Integer> intBox = new Box<>();
intBox.setContent(123);
int num = intBox.getContent();

上下届限定符

< ? extends T > 指的是 上界通配符 指的是泛型中的类必须为当前类或为当前类的子类

< ? super T> 指的是 下界通配符 指的是泛型中的类必须为当前类或为当前类的父类

List<Object> 和 List<?>的区别

List<?> 是一个未知类型的 List,而 List<Object> 其实是任意类型的 List。

你可以把 List<String>, List<Integer>赋值给 List<?>,却不能把 List<String>赋值给 List<Object>。

List<Object> 和 List 的区别

List 叫原始类型

在编译的时候 编译器不会对原始类型进行类型安全检查 却会对带参数的类型进行检查

通过 Object 作为类型 我们可以告诉编译器该方法可以接受任意类型的对象

你可以把任何带参数的类型传递给原始类型 List,但却不能 把 List<String>传递给接受 List<Object>的方法,因为会产生编译错误。

类型擦除

< ? extends T > 指的是 上界通配符 指的是范型中的类必须为当前类或为当前类的子类

泛型与编译的关系

Java 泛型是一个编译器层面存在的语法糖 所有逻辑只会在编译期间完成 运行时不会保留泛型信息

要理解泛型在编译时的差异(尤其是 List<Object>List<?>),核心在于 编译器如何处理类型信息 。Java 泛型的核心机制是 类型擦除(Type Erasure),但擦除前后的编译检查逻辑才是差异的关键。

一、先明确:泛型的"编译时核心作用"

Java 泛型本质是 编译器的语法糖 ,它的所有逻辑(类型检查、转换)都在 编译阶段 完成,运行时不会保留泛型参数信息(类型擦除)。

编译器的核心任务是:在编译时确保"泛型操作的类型安全" ,避免运行时出现 ClassCastException

二、List<Object>List<?> 在编译时的差异

我们从 类型检查规则类型擦除后的结果 两个角度对比:

1. 类型检查规则:编译器如何判断"操作是否合法"?

编译器对 List<Object>List<?> 的检查逻辑完全不同,这直接导致了两者的使用限制差异。

(1)List<Object> 的编译检查:明确允许"所有 Object 及其子类"

List<Object> 中的泛型参数 <Object> 是一个 明确的类型,编译器知道:

  • 这个集合"声明为存放 Object 类型"(由于所有类都是 Object 的子类,所以实际可以放任何对象)。
  • 因此,对 List<Object> 的操作,只要符合"元素是 Object 或其子类",就被允许。

具体表现

  • 添加元素 :可以添加任何类型(因为任何对象都能向上转型为 Object)。
    编译器会检查添加的元素是否能转型为 Object(显然都能),所以允许:

    List<Object> objList = new ArrayList<>();
    objList.add("hello"); // 合法:String → Object 转型安全
    objList.add(123); // 合法:Integer → Object 转型安全

  • 接收赋值 :只能接收 List<Object> 类型。
    因为 List<String>List<Object> 没有继承关系(即使 String 是 Object 的子类,List<String> 也不是 List<Object> 的子类),编译器会阻止这种赋值:

    List<String> strList = new ArrayList<>();
    List<Object> objList = strList; // 编译错误:类型不兼容

(编译器会认为:List<String> 只能存 String,而 List<Object> 可以存任何对象,若允许赋值,可能导致 objList.add(123) 破坏 strList 的类型安全。)

(2)List<?> 的编译检查:"未知类型"导致的严格限制

List<?> 中的 ? 表示 未知类型(可以是任何类型,但编译器不知道具体是哪一种)。编译器为了保证类型安全,会对其操作做严格限制:

  • 添加元素 :几乎禁止添加任何具体类型(除了 null)。
    因为编译器不知道 ? 代表什么类型,假设添加 String,但实际 List<?> 可能是 List<Integer>(此时添加 String 会出错);同理添加任何类型都可能不安全。因此编译器直接禁止:

    List<?> anyList = new ArrayList<String>();
    anyList.add("hello"); // 编译错误:无法确定 ? 是否接受 String
    anyList.add(null); // 合法:null 可以是任何类型

  • 接收赋值 :可以接收任何泛型 List(如 List<String>List<Integer>)。
    因为 List<?> 表示"某种未知类型的 List",而 List<String> 本质上是"String 类型的 List",属于"未知类型"的一种可能,因此允许赋值:

    List<String> strList = new ArrayList<>();
    List<?> anyList = strList; // 合法:strList 是"某种具体类型的 List",符合 ? 的定义

  • 获取元素 :获取的元素只能被当作 Object 处理。
    因为编译器不知道 ? 是什么类型,只能确定它一定是 Object 的子类(Java 中所有类都继承 Object),所以获取元素时会自动向上转型为 Object:

    List<?> anyList = new ArrayList<String>();
    Object obj = anyList.get(0); // 合法:无论 ? 是什么类型,都能转成 Object
    String str = anyList.get(0); // 编译错误:无法确定 ? 是 String

2. 类型擦除后的差异:编译后泛型信息如何消失?

Java 泛型在编译后会进行 类型擦除 :泛型参数会被替换为"原始类型",同时添加必要的类型转换代码。
List<Object>List<?> 擦除后的结果不同:

  • List<Object> 擦除后 → List(原始类型),但保留了"元素是 Object"的隐含信息(因为擦除前明确是 Object)。
    例如:

    List<Object> list = new ArrayList<>();
    list.add("hello"); // 擦除后:add(Object obj),参数自动转型为 Object
    Object obj = list.get(0); // 擦除后:get() 返回 Object,无需额外转换

  • List<?> 擦除后 → 也是 List(原始类型),但由于 ? 是未知类型,编译器会在必要时插入更严格的类型转换检查。
    例如:

    List<?> anyList = new ArrayList<String>();
    Object obj = anyList.get(0); // 擦除后:get() 返回 Object(和上面一样)

(虽然擦除后结果相同,但编译时的检查逻辑完全不同,这才是关键。)

三、总结:编译时差异的核心

|---------|---------------------------|-----------------------------|
| 对比维度 | List<Object> | List<?> |
| 泛型含义 | 明确声明为"存放 Object 类型" | 声明为"存放未知类型"(具体类型不确定) |
| 编译时检查重点 | 确保元素是 Object 或其子类(宽松) | 确保不破坏未知类型的安全性(严格) |
| 允许添加元素 | 任何类型(因都能转 Object) | 几乎不允许(除 null,因未知类型无法匹配) |
| 允许接收的赋值 | 只能是 List<Object> | 任何泛型 List(如 List<String>) |
| 获取元素的类型 | 可直接当作 Object 或向下转型(需显式强转) | 只能当作 Object(无法确定具体类型) |

简单说:List<Object> 在编译时明确"我能存任何东西",所以操作灵活;List<?> 明确"我不知道能存什么",所以编译器为了安全,限制了大部分修改操作。这种差异完全是编译器在编译阶段通过类型检查规则实现的,和运行时无关。泛型擦除(Type Erasure)后,<? extends T><? super T>通配符边界信息会被擦除 ,但编译器在编译时基于这些边界执行的类型检查逻辑 ,会转化为运行时的字节码指令(主要是类型转换),间接体现了通配符的约束效果。

简单说:擦除后不会"保留"通配符的边界逻辑,但编译时基于边界做的检查,会通过生成的字节码(如强制转型)在运行时产生类似"遵循边界"的效果。

四、擦除前后的变化

1. <? extends T> 的擦除与逻辑体现
  • 编译时<? extends T> 限制"只能读取元素(作为 T 类型),不能添加除 null 外的元素"(因为编译器不知道具体是 T 的哪个子类,添加元素可能破坏类型安全)。

  • 擦除后 :泛型信息被移除,List<? extends T> 会被擦除为原始类型 List,但编译器会在读取元素时自动插入 (T)强制转型,确保返回值符合 T 类型。示例:

    List<? extends Number> numList = new ArrayList<Integer>();
    Number n = numList.get(0); // 编译时允许(符合 extends 约束)

擦除后字节码等价于:

复制代码
List numList = new ArrayList(); // 原始类型
Number n = (Number) numList.get(0); // 编译器自动添加 (Number) 转型

这里的转型本质是编译时基于 extends Number边界的保证 :既然 numList 的元素是 Number 的子类,那么转型为 Number 一定安全。

2. <? super T> 的擦除与逻辑体现
  • 编译时<? super T> 限制"可以添加 T 或其子类的元素,但读取元素时只能作为 Object 处理"(因为编译器不知道具体是 T 的哪个父类,读取时无法确定更具体的类型)。

  • 擦除后 :同样被擦除为原始类型 List,但编译器会在添加元素时检查是否为 T 或其子类(编译时保证),读取时则默认返回 Object(无需转型,或由开发者显式转型)。示例:

    List<? super Integer> intList = new ArrayList<Number>();
    intList.add(123); // 编译时允许(123 是 Integer 类型,符合 super 约束)
    Object obj = intList.get(0); // 编译时只能作为 Object 接收

擦除后字节码等价于:

复制代码
List intList = new ArrayList(); // 原始类型
intList.add(123); // 编译时已检查 123 是 Integer,允许添加(无转型必要)
Object obj = intList.get(0); // 直接返回 Object(无需转型)

这里的添加操作安全,是因为编译时基于 super Integer边界的保证intList 的元素类型是 Integer 的父类(如 Number、Object),添加 Integer 一定兼容。

五、总结:关键结论

  1. 擦除后不保留通配符本身<? extends T><? super T> 擦除后都会变成原始类型(如 List),边界信息(extends T/super T)不会保留在运行时。
  2. 编译时检查转化为运行时转型 :通配符的边界逻辑主要体现在编译时的类型检查 (如允许/禁止添加元素、确定读取元素的类型),这些检查会转化为运行时的强制转型指令(如 (T)),从而间接实现"遵循边界"的效果。
  3. 核心是编译时的安全保证 :擦除后能"不出错",本质是编译器在编译阶段已经通过通配符边界排除了不安全的操作(如给 <? extends T> 添加非 null 元素会编译报错),运行时的转型只是"兑现"了编译时的安全承诺。

简单说:通配符的边界逻辑是编译器的"设计规范",擦除后通过字节码中的转型操作"落地",最终保证运行时的类型安全。

相关推荐
lht63193561216 小时前
从Windows通过XRDP远程访问和控制银河麒麟 v10服务器
linux·运维·服务器·windows
阿豪学编程16 小时前
环境变量与程序地址空间
linux·运维·windows
墨倾许17 小时前
《Windows 11 + Docker:极简DVWA靶场搭建全记录》—— 附详细排错指南与最终解决方案
windows·笔记·网络安全·docker·容器·靶场
stark张宇19 小时前
盘点Nfs 文件服务在Windows上的坑??
linux·windows·centos
杨凯凡21 小时前
Docker环境搭建:Windows/macOS/Linux全平台教程
windows·macos·docker
阿昭L21 小时前
COM组件
windows
歪歪1001 天前
解决多 Linux 客户端向 Windows 服务端的文件上传、持久化与生命周期管理问题
linux·运维·服务器·开发语言·前端·数据库·windows
山川而川-R1 天前
ubuntu摄像头型号匹配不上_11-6
linux·windows·ubuntu
一般社员1 天前
Windows导入大型sql文件到mysql
windows·sql·mysql