是的,java泛型在编译后被擦除,字节码中仅保留原始类型,类型参数全部替换为object或上界,运行时无法获取泛型信息,仅通过class元数据中的signature属性供反射等有限使用。

泛型在编译后真的“消失”了吗
是的,Java泛型在字节码层面完全不存在——ArrayList<string></string> 和 ArrayList<integer></integer> 编译后都是裸的 ArrayList。这不是JVM懒,而是设计选择:为了兼容 JDK 1.4 及更早版本的类库和字节码格式。
编译器做的其实是“类型检查 + 插入强制转换”。比如你写 list.get(0),编译器会根据声明类型(如 String)自动补上 (String) 强转;但这个转换发生在运行时,且不带任何类型信息校验。
- 所有泛型类型参数(
T、E、K等)在运行时都变成Object或对应上界(如class Box<t extends number></t>中的T擦除为Number) - 泛型方法的类型参数同样被擦除,
<t> T pick(T a, T b)</t>编译后签名是Object pick(Object a, Object b) -
instanceof、new T()、T.class全部非法——因为运行时根本没T这个东西
为什么不能 new ArrayList().getClass() == new ArrayList().getClass()
因为它们的运行时 Class 对象确实是同一个:ArrayList.class。泛型信息只保留在类文件的 Signature 属性里,供反射(如 Method.getGenericReturnType())或编译器读取,JVM 加载时不解析也不使用。
这导致几个典型问题:
立即学习“Java免费学习笔记(深入)”;
- 无法通过
obj.getClass()区分泛型实例,哪怕类型参数完全不同 -
Class.isAssignableFrom()和Class.isInstance()对泛型无感知,只看原始类型 - 序列化/反序列化时(如 JSON),若没额外传类型信息(如
TypeReference),默认按Object处理,容易丢精度
怎么绕过擦除获取真实类型(有限但实用)
唯一靠谱的路径是利用“泛型声明未被擦除”这一事实——子类继承带具体类型的泛型父类时,父类的类型实参会被记在子类的 Class 元数据中,可通过反射提取。
常见用法:
- 定义匿名子类:
new ArrayList<string>() {}</string>,再用((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()拿到String.class - Gson 的
TypeToken<list>>() {}</list>就是基于这个技巧 - Spring 的
ResolvableType.forInstance(obj)内部也大量依赖此类反射逻辑 - 注意:仅对**编译期已知的具体类型**有效;运行时动态构造的泛型(如拼接字符串再加载类)依然不可查
泛型擦除带来的实际坑(别等报错才反应过来)
最常踩的是“看似类型安全,实则运行时崩”。比如下面这段代码能编译通过,但会在运行时抛 ClassCastException:
ArrayList<String> strings = new ArrayList<>(); ArrayList raw = strings; // 转成原生类型 raw.add(123); // OK,擦除后就是 add(Object) String s = strings.get(0); // ClassCastException!
其他高频陷阱:
- 重载冲突:
void foo(List<string>)</string>和void foo(List<integer>)</integer>编译失败——擦除后都是foo(List),无法共存 - 静态泛型字段共享:
class Box<t> { static T t; }</t>非法,因为静态变量属于类而非实例,而擦除后所有Box共享同一份静态区 - 数组创建限制:
new ArrayList<string>[10]</string>合法,但new ArrayList<string>[10][]</string>会触发 unchecked 警告,因运行时无法保证元素类型安全
泛型擦除不是 bug,是权衡。它让 Java 在不改动 JVM 的前提下引入类型安全,代价是牺牲了运行时的类型完整性和部分表达力。真正难的不是理解擦除,而是记住哪些地方“看起来有类型,其实没有”。










