乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

类型擦除

"类型擦除是Java泛型的'隐形成本'——它让泛型与旧代码和平共处,但也设下了诸多限制。不理解擦除,就无法真正理解泛型。" —— 大翔,飞翔科技CTO


1. 类型擦除的定义和本质

类型擦除(Type Erasure) 是Java泛型的实现机制:编译器在编译过程中,将所有泛型类型信息移除,用原始类型(Raw Type)或边界类型替代,并在必要位置插入类型转换指令。

核心事实

重要结论:List<String>和List<Integer>在运行时是同一个类——List.class。泛型类型参数String和Integer在运行时完全不存在。

一句话总结

泛型是编译期概念。编译器检查完类型安全后,将泛型信息擦除干净,JVM看到的只是普通的类和方法调用。


2. 三种擦除规则

擦除场景擦除规则示例
无界类型参数 <T>替换为 ObjectBox<T> → Box,字段 T value → Object value
有界类型参数 <T extends Number>替换为边界类型NumBox<T extends Number> → NumBox,字段 T value → Number value
方法签名中的泛型类型参数被擦除,必要时生成桥接方法T get() → Object get()

2.1 无界类型擦除 → Object

// 源代码
public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

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

编译后等效代码(类型擦除完成):

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

    public Box(Object value) {  // T 被替换为 Object
        this.value = value;
    }

    public Object getValue() {  // 返回类型 T → Object
        return value;
    }

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

反编译验证

将Box<T>编译后,用javap -c -v Box.class查看字节码:

$ javap -c -v Box.class

Classfile /.../Box.class
  ...
  #29 = Fieldref           #3.#30   // Box.value:Ljava/lang/Object;  ← 注意!字段类型是Object
  ...

  public java.lang.Object getValue();  ← 返回值被擦除为Object
    descriptor: ()Ljava/lang/Object;
    Code:
       0: aload_0
       1: getfield      #29    // Field value:Ljava/lang/Object;
       4: areturn

  public void setValue(java.lang.Object);  ← 参数被擦除为Object
    descriptor: (Ljava/lang/Object;)V;

可以看到,字节码层面只有Object,毫无T的踪迹。

2.2 有界类型擦除 → 边界类型

// 源代码:T 被限制为 Number 的子类型
public class NumberBox<T extends Number> {
    private T value;

    public NumberBox(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public double doubleValue() {
        return value.doubleValue();  // 可以调用Number的方法!
    }
}

编译后等效代码:

public class NumberBox {
    private Number value;       // T extends Number → Number

    public NumberBox(Number value) {
        this.value = value;
    }

    public Number getValue() {  // 返回类型 → Number
        return value;
    }

    public double doubleValue() {
        return value.doubleValue();  // 可以安全调用Number的方法
    }
}

关键区别:

  • 无界<T> → Object:字段只能调用Object的方法(toString()、equals()等)
  • 有界<T extends Number> → Number:字段可以调用Number的方法(intValue()、doubleValue()等)
// 测试代码
public class NumberBoxTest {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>(100);
        NumberBox<Double> doubleBox = new NumberBox<>(3.14);

        System.out.println("Integer盒子:" + intBox.getValue());
        System.out.println("Double盒子:" + doubleBox.getValue());
        System.out.println("Integer的double值:" + intBox.doubleValue());
        System.out.println("Double的double值:" + doubleBox.doubleValue());

        // 下面这行无法编译 —— String不是Number的子类
        // NumberBox<String> strBox = new NumberBox<>("hello");  // ❌ 编译错误!
    }
}

运行输出:

Integer盒子:100
Double盒子:3.14
Integer的double值:100.0
Double的double值:3.14

3. 桥接方法

桥接方法(Bridge Method) 是编译器自动生成的合成方法,用于在类型擦除后保持多态性。

产生原因

考虑以下场景——一个泛型类实现了泛型接口:

// 泛型接口
public interface Repository<T> {
    void save(T entity);
}

// 实现类:明确指定 T=Student
public class StudentRepository implements Repository<Student> {
    @Override
    public void save(Student entity) {    // 开发者写的实际方法
        System.out.println("保存学生:" + entity.getName());
    }
}

问题来了:类型擦除后,接口方法签名为void save(Object entity),而实现类方法签名为void save(Student entity)——这两个方法的参数类型不同,从JVM角度看是不同的方法,不是重写!多态将失效。

编译器的解决方案:生成桥接方法

编译器在StudentRepository中自动插入一个桥接方法:

// 编译后等效代码(包括编译器自动生成的桥接方法)
public class StudentRepository implements Repository {
    
    // 【桥接方法】编译器自动生成,参数类型与接口擦除后一致
    public void save(Object entity) {          // 桥接方法:匹配接口的 save(Object)
        this.save((Student) entity);           // 强制转换后调用实际方法
    }
    
    // 【实际方法】开发者编写的
    public void save(Student entity) {         // 实际业务逻辑
        System.out.println("保存学生:" + entity.getName());
    }
}

反编译验证

$ javap -c StudentRepository.class

Compiled from "StudentRepository.java"
public class StudentRepository implements Repository<Student> {
  public StudentRepository();
    ...

  // 桥接方法!
  public void save(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC       ← 注意这两个标志!
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #21   // class Student         ← 强制转换
       5: invokevirtual #23   // Method save:(LStudent;)V  ← 调用实际方法
       8: return

  // 实际方法
  public void save(Student);
    descriptor: (LStudent;)V
    flags: ACC_PUBLIC
    Code:
       0: getstatic     #30   // Field System.out
       3: new           #36   // StringBuilder
       ...

字节码中出现了两个save方法——一个接受Object参数(桥接方法),另一个接受Student参数(实际方法)。桥接方法携带ACC_BRIDGE和ACC_SYNTHETIC标志。

桥接方法的完整示例

/**
 * 飞翔科技场景:展示桥接方法机制
 */
// 泛型接口
interface Converter<T> {
    T convert(String source);
}

// 实现类:指定 T=Integer
class StringToIntConverter implements Converter<Integer> {
    @Override
    public Integer convert(String source) {
        return Integer.parseInt(source);
    }
}

// 测试:通过反射查看方法
public class BridgeMethodDemo {
    public static void main(String[] args) {
        StringToIntConverter converter = new StringToIntConverter();

        // 多态调用 —— 正常
        Converter<Integer> polyRef = converter;
        Integer result = polyRef.convert("1024");
        System.out.println("转换结果:" + result);

        // 反射查看所有方法
        System.out.println("\n=== 反射查看方法 ===");
        for (java.lang.reflect.Method m : StringToIntConverter.class.getMethods()) {
            System.out.println("方法名:" + m.getName());
            System.out.println("  返回类型:" + m.getReturnType().getSimpleName());
            System.out.println("  参数类型:" + java.util.Arrays.toString(m.getParameterTypes()));
            System.out.println("  是否桥接:" + m.isBridge());
            System.out.println("  是否合成:" + m.isSynthetic());
            System.out.println();
        }
    }
}

运行输出:

转换结果:1024

=== 反射查看方法 ===
方法名:convert
  返回类型:Integer
  参数类型:[class java.lang.String]
  是否桥接:false
  是否合成:false

方法名:convert
  返回类型:Object
  参数类型:[class java.lang.String]
  是否桥接:true
  是否合成:true

可以看到,反射列出了两个convert方法——一个带Integer返回值(实际方法),一个带Object返回值(桥接方法)。


4. 类型擦除带来的限制

由于泛型信息在运行时被完全擦除,以下操作在Java中是不允许的。

限制一:不能 new T()

// ❌ 错误:编译错误!Type parameter 'T' cannot be instantiated directly
public class Factory<T> {
    public T create() {
        return new T();  // 编译错误!
    }
}

原因:类型擦除后T变成Object,new Object()虽然合法,但这不是你想要的对象。更重要的是,编译器无法知道T是否有无参构造器。

解决方案:传入Class<T>对象或使用工厂模式。

// ✅ 方案一:通过 Class<T> 反射创建
public class Factory<T> {
    private Class<T> type;

    public Factory(Class<T> type) {
        this.type = type;
    }

    public T create() throws Exception {
        return type.newInstance();  // 反射方式(JDK 8可用)
    }
}

// ✅ 方案二:传入 Supplier(JDK 8函数式接口)
public class Factory<T> {
    private Supplier<T> supplier;

    public Factory(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}

// 使用示例
Factory<Student> factory = new Factory<>(Student::new);  // 构造函数引用
Student s = factory.create();

限制二:不能 instanceof 泛型类型

// ❌ 错误:编译错误!illegal generic type for instanceof
public <T> boolean isListOfString(Object obj) {
    if (obj instanceof List<String>) {  // 编译错误!
        return true;
    }
    return false;
}

原因:运行时没有List<String>这个类型,只有List。JVM无法区分List<String>和List<Integer>。

// ✅ 正确:使用无界通配符
public <T> boolean isList(Object obj) {
    if (obj instanceof List<?>) {  // 合法
        return true;
    }
    return false;
}

限制三:不能创建泛型数组

// ❌ 错误:编译错误!generic array creation
public class Repository<T> {
    private T[] items = new T[10];     // 编译错误!
    private List<T>[] listArray = new List<T>[5];  // 编译错误!
}

原因:数组在运行时持有其组件类型信息,而T在运行时不存在。Java数组是协变的(String[]是Object[]的子类型),如果允许泛型数组,会破坏类型安全。

// ✅ 方案一:使用 ArrayList 代替数组
public class Repository<T> {
    private List<T> items = new ArrayList<>();
}

// ✅ 方案二:通过反射创建(需要传入 Class<T>)
public class Repository<T> {
    private T[] items;

    @SuppressWarnings("unchecked")
    public Repository(Class<T> type, int size) {
        this.items = (T[]) Array.newInstance(type, size);  // 反射创建
    }
}

// ✅ 方案三:声明Object数组,取出时强制转换(集合框架的做法)
public class Repository<T> {
    private Object[] items;

    public Repository(int size) {
        this.items = new Object[size];
    }

    @SuppressWarnings("unchecked")
    public T get(int index) {
        return (T) items[index];  // 强制转换
    }
}

ArrayList<T>内部正是使用方案三——private Object[] elementData。

限制四:静态上下文不能用类型参数

public class Repository<T> {
    // ❌ 错误:non-static type variable T cannot be referenced from a static context
    private static T defaultEntity = null;     // 编译错误!

    // ❌ 错误
    public static T createDefault() {          // 编译错误!
        return null;
    }

    // ❌ 错误
    private static List<T> cache = new ArrayList<>();  // 编译错误!
}

原因:T依赖实例化时的具体类型——Repository<Student>和Repository<Course>有不同的T。静态成员属于类,在所有实例间共享,无法确定用哪个类型。

// ✅ 正确:静态方法可以声明自己的类型参数
public class Repository<T> {
    public static <E> List<E> emptyList() {    // <E> 是方法自己的类型参数
        return Collections.emptyList();
    }
}

限制五:不能重载仅泛型参数不同的方法

// ❌ 错误:两个方法在类型擦除后签名完全相同!
public class Processor {
    public void process(List<String> strings) { }   // 擦除后:process(List)
    public void process(List<Integer> ints) { }     // 擦除后:process(List) —— 冲突!
}

编译错误:name clash: process(List<Integer>) and process(List<String>) have the same erasure

// ✅ 正确:方法名不同,或参数列表在擦除后可区分
public class Processor {
    public void processStrings(List<String> strings) { }
    public void processIntegers(List<Integer> ints) { }
}

限制六:不能捕获泛型类型异常

// ❌ 错误:泛型类不能继承Throwable
public class GenericException<T> extends Exception { }  // 编译错误!

// ❌ 错误:不能catch泛型类型参数
public <T extends Exception> void handle() {
    try {
        // ...
    } catch (T e) {     // 编译错误!
        // ...
    }
}
// ✅ 正确:可以在throws中使用类型参数
public <T extends Exception> void execute() throws T {  // 合法
    // ...
}

5. 易错场景

反例一:试图通过反射获取泛型类型参数

小崔听到类型擦除后,想知道运行时能否通过反射拿到T的类型:

// ❌ 错误:反射也拿不到类型参数!
Box<Student> studentBox = new Box<>(new Student("小崔", 22));
Class<?> clazz = studentBox.getClass();

// 获取字段
Field valueField = clazz.getDeclaredField("value");
System.out.println("字段类型:" + valueField.getType());
// 输出:class java.lang.Object   ← 不是Student!

纠正:对于局部变量和字段的泛型声明,确实无法在运行时获取。但可以通过反射获取超类的泛型参数:

// ✅ 通过匿名子类保留泛型信息
Box<Student> studentBox = new Box<Student>(new Student("小崔", 22)) {};  // 注意末尾的{}

// 获取泛型超类
Type superClass = studentBox.getClass().getGenericSuperclass();
if (superClass instanceof ParameterizedType) {
    ParameterizedType pt = (ParameterizedType) superClass;
    Type[] actualTypeArgs = pt.getActualTypeArguments();
    System.out.println("泛型参数:" + actualTypeArgs[0]);
    // 输出:泛型参数:class com.feixiang.model.Student
}

这种技巧被广泛用于Spring的RestTemplate、Gson的TypeToken、Jackson的TypeReference等框架中。

反例二:原始类型绕过编译检查

// ❌ 危险操作:原始类型污染泛型集合
List<String> safeList = new ArrayList<>();
safeList.add("安全字符串");

List rawList = safeList;     // 赋值给原始类型(编译警告,不报错!)
rawList.add(42);             // 放入Integer!编译警告,运行时成功

// 此时 safeList 中既有String又有Integer
String s = safeList.get(1);  // ❌ ClassCastException!
// ✅ 正确:绝不将泛型对象赋值给原始类型变量
// 如果必须与遗留代码交互,使用 Collections.checkedList 增加运行时检查
List<String> safeList = Collections.checkedList(
    new ArrayList<>(),
    String.class
);
safeList.add("安全字符串");

// 即使通过原始类型操作,checkedList也会在add时检查
List rawList = safeList;
rawList.add(42);  // ❌ 这次会在运行时抛出ClassCastException,而不是默默污染集合

6. 面试考点

Q1:什么是类型擦除?为什么会存在?

类型擦除是Java泛型的实现方式:编译器在编译后将所有泛型类型信息移除,用原始类型或边界类型替代。存在原因是向后兼容——JDK 5之前的字节码中不存在泛型信息,擦除让泛型代码能在不修改JVM的情况下与旧代码平滑共存。

Q2:List<String>和List<Integer>的Class对象是同一个吗?验证方法是什么?

是同一个。可以验证:List<String>.class在Java中不允许,但可以通过两个实例的getClass()比较:

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());  // true

Q3:什么是桥接方法?什么时候产生?

桥接方法是编译器自动生成的合成方法,用于保持类型擦除后的多态性。当子类实现或重写泛型父类方法时,如果擦除后的方法签名不匹配,编译器会在子类中生成与父类擦除后签名相同的桥接方法,内部强制转换后调用实际方法。字节码标志为ACC_BRIDGE和ACC_SYNTHETIC。

Q4:类型擦除带来了哪些限制?

① 不能new T()——运行时不知道T的具体类型;② 不能instanceof泛型类型——运行时不存在List<String>;③ 不能创建泛型数组new T[n]——数组在运行时持有组件类型;④ 静态上下文不能使用类的类型参数——静态成员属于类而非实例;⑤ 不能重载仅泛型参数不同的方法——擦除后签名相同;⑥ 泛型类不能继承Throwable,不能catch泛型类型参数。

Q5:类型擦除后,编译器如何保证运行时类型安全?

编译器在擦除泛型信息的同时,会在必要位置自动插入checkcast字节码指令。例如list.get(0)取出的明明是Object,但由于编译器知道该list的元素类型是String,会在取出后插入checkcast #String指令。如果运行时类型不匹配,JVM会立即抛出ClassCastException,而不是让不匹配的对象在程序中继续传播。

上一页
通配符与PECS原则