Java复习之范型相关 类型擦除
目录
上下届限定符
List
类型擦除
泛型与编译的关系
一、先明确:泛型的“编译时核心作用”
1. 类型检查规则:编译器如何判断“操作是否合法”?
(1)List
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(和上面一样)
(虽然擦除后结果相同,但编译时的检查逻辑完全不同,这才是关键。)
三、总结:编译时差异的核心
| 对比维度 |
|
|
| 泛型含义 | 明确声明为“存放 Object 类型” | 声明为“存放未知类型”(具体类型不确定) |
| 编译时检查重点 | 确保元素是 Object 或其子类(宽松) | 确保不破坏未知类型的安全性(严格) |
| 允许添加元素 | 任何类型(因都能转 Object) | 几乎不允许(除 null,因未知类型无法匹配) |
| 允许接收的赋值 | 只能是 | 任何泛型 List(如 |
| 获取元素的类型 | 可直接当作 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 一定兼容。
五、总结:关键结论
- 擦除后不保留通配符本身:
<? extends T>和<? super T>擦除后都会变成原始类型(如List),边界信息(extends T/super T)不会保留在运行时。 - 编译时检查转化为运行时转型:通配符的边界逻辑主要体现在编译时的类型检查(如允许/禁止添加元素、确定读取元素的类型),这些检查会转化为运行时的强制转型指令(如
(T)),从而间接实现“遵循边界”的效果。 - 核心是编译时的安全保证:擦除后能“不出错”,本质是编译器在编译阶段已经通过通配符边界排除了不安全的操作(如给
<? extends T>添加非 null 元素会编译报错),运行时的转型只是“兑现”了编译时的安全承诺。
简单说:通配符的边界逻辑是编译器的“设计规范”,擦除后通过字节码中的转型操作“落地”,最终保证运行时的类型安全。
