Java泛型擦除的7个坑:List<String>为什么不等于List<Object>
Java泛型擦除的7个坑:List<String>为什么不等于List<Object>
适读人群:Java中级开发者、用过泛型但踩过奇怪坑的后端工程师 | 阅读时长:约15分钟 | 文章类型:原理剖析+坑点整理
开篇故事
几年前我刚接手一个老项目,里面有一段工具代码是这样的:
public class JsonUtils {
public static <T> T fromJson(String json, Class<T> clazz) {
return objectMapper.readValue(json, clazz);
}
}调用方这么用:
List<Order> orders = JsonUtils.fromJson(json, List.class); // 编译通过上线跑了一段时间,某次遍历orders时抛了个让人摸不着头脑的异常:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.Order
at com.example.OrderService.processOrders(OrderService.java:45)当时我盯着这个报错看了好久。LinkedHashMap是怎么跑进去的?明明指定了List<Order>……
根本原因是泛型擦除。List.class在运行时就是List.class,没有泛型信息。Jackson拿不到Order类型,把JSON对象默认反序列化成了LinkedHashMap。
这是泛型擦除带来的经典坑之一。今天把我踩过的7个坑系统说一遍。
一、泛型擦除是什么
Java泛型是在JDK 5引入的,为了保持与旧版本的兼容性,Java选择了一种叫做类型擦除(Type Erasure)的实现方式。
简单说:泛型类型信息只存在于编译期,编译器用它来做类型检查;编译之后,字节码里的泛型信息被擦除了,替换成了原始类型(Object或边界类型)。
// 源码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 编译后,字节码里都变成了
List stringList = new ArrayList();
List intList = new ArrayList();这就导致了一个让人费解的事实:
System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass());
// 输出:true在运行时,ArrayList<String>和ArrayList<Integer>是同一个类。
二、核心原理深挖
泛型擦除的规则
编译器擦除泛型参数时,遵循以下规则:
- 无界类型参数(
<T>)擦除为Object - 有界类型参数(
<T extends Comparable>)擦除为第一个边界(Comparable) - 通配符(
<? extends Number>)擦除为边界类型(Number)
// 源码
public <T> T getFirst(List<T> list) { return list.get(0); }
public <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; }
// 擦除后(概念上)
public Object getFirst(List list) { return list.get(0); }
public Comparable max(Comparable a, Comparable b) { return a.compareTo(b) > 0 ? a : b; }编译器同时会在需要的地方插入类型转换(checkcast)字节码,保证类型安全。
为什么List<String>不能赋给List<Object>
这是很多人困惑的地方。String是Object的子类,为什么List<String>不是List<Object>的子类?
答案是:如果允许,会破坏类型安全。
// 假设这是合法的(实际上编译器会报错)
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 假设允许
objects.add(42); // 向List<Object>添加Integer
String s = strings.get(0); // ClassCastException!如果允许这种赋值,就能绕过类型检查,往List<String>里塞进任意类型。所以Java设计成:泛型是不可变(invariant)的,List<String>和List<Object>没有继承关系。
通配符:带界限的灵活性
为了解决不可变带来的限制,Java引入了通配符:
List<?>— 无界通配符,可读不可写(写null除外)List<? extends T>— 上界通配符,可读,读出来是T类型,不可写List<? super T>— 下界通配符,可写T类型,读出来只能是Object
三、7个具体的坑与解决方案
代码一:复现并解决7个典型坑
package com.laozhang.trap.generics;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.*;
import java.util.*;
/**
* Java泛型擦除的7个典型坑
*/
public class GenericsErasureTrap {
private static final ObjectMapper objectMapper = new ObjectMapper();
// ======================== 坑1:instanceof不能用于泛型 ========================
public void trap1() {
List<String> list = new ArrayList<>();
// 编译错误!Cannot perform instanceof check against parameterized type List<String>
// if (list instanceof List<String>) { ... }
// 只能检查原始类型
if (list instanceof List) {
System.out.println("是List"); // 只知道是List,不知道元素类型
}
// 正确做法:检查元素类型
if (list instanceof List && !((List<?>) list).isEmpty()) {
Object first = ((List<?>) list).get(0);
if (first instanceof String) {
System.out.println("第一个元素是String");
}
}
}
// ======================== 坑2:不能创建泛型数组 ========================
public <T> void trap2() {
// 编译错误!Cannot create a generic array of T
// T[] arr = new T[10];
// 解法1:用Object数组,强转
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10]; // 有unchecked警告,运行时类型是Object[]
// 解法2:用List替代数组
List<T> list = new ArrayList<>();
// 解法3:传入Class<T>,用反射创建
}
@SuppressWarnings("unchecked")
public <T> T[] createArray(Class<T> clazz, int size) {
return (T[]) Array.newInstance(clazz, size);
}
// ======================== 坑3:JSON反序列化泛型集合 ========================
// 错误方式:运行时拿不到Order类型,返回List<LinkedHashMap>
@SuppressWarnings("unchecked")
public List<Order> trap3Wrong(String json) throws Exception {
return objectMapper.readValue(json, List.class); // 丢失泛型,实际是List<LinkedHashMap>
}
// 正确方式1:使用TypeReference
public List<Order> trap3Correct1(String json) throws Exception {
return objectMapper.readValue(json, new TypeReference<List<Order>>() {});
// TypeReference通过匿名子类保存了泛型信息,运行时可通过反射获取
}
// 正确方式2:JavaType
public List<Order> trap3Correct2(String json) throws Exception {
return objectMapper.readValue(json,
objectMapper.getTypeFactory().constructCollectionType(List.class, Order.class));
}
// ======================== 坑4:泛型方法的类型推断陷阱 ========================
// 看似返回T,实际上有ClassCastException风险
@SuppressWarnings("unchecked")
public static <T> T unsafeCast(Object obj) {
return (T) obj; // 这里的转换在字节码里是noop,因为T被擦除了
}
public void trap4() {
// 编译通过,运行时ClassCastException
String s = GenericsErasureTrap.<String>unsafeCast(42); // 传了Integer,接收String
// 不是在这行报错,而是在真正把它当String使用时报错
System.out.println(s.length()); // ClassCastException here
}
// ======================== 坑5:泛型类的静态方法不能使用类的泛型参数 ========================
static class Container<T> {
private T value;
// 错误:静态方法不能用类的泛型参数T
// public static T getDefault() { return null; } // 编译错误
// 正确:静态泛型方法有自己的类型参数
public static <E> E getDefault(Class<E> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
return null;
}
}
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
}
// ======================== 坑6:运行时泛型信息获取(通过反射) ========================
public void trap6() throws Exception {
// 获取字段的泛型类型
Field field = this.getClass().getDeclaredField("orders");
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) genericType;
System.out.println("原始类型: " + pt.getRawType()); // interface java.util.List
Type[] typeArgs = pt.getActualTypeArguments();
System.out.println("类型参数: " + typeArgs[0]); // class com.example.Order
}
}
private List<Order> orders; // 字段的泛型信息在.class文件里以Signature属性保存
// ======================== 坑7:重载方法的泛型擦除冲突 ========================
// 编译错误!erasure相同:都是 process(List)
// public void process(List<String> list) { ... }
// public void process(List<Integer> list) { ... }
// 解决方案1:改方法名
public void processStrings(List<String> list) { /* ... */ }
public void processIntegers(List<Integer> list) { /* ... */ }
// 解决方案2:用不同的参数位置区分
public void process(List<String> strings, Class<String> type) { /* ... */ }
public void process(List<Integer> integers, Class<Integer> type) { /* ... */ }
static class Order {
private String id;
private String name;
// getter/setter...
}
}代码二:TypeReference的实现原理——运行时保留泛型信息
package com.laozhang.trap.generics;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* 手写TypeReference,理解Jackson如何在运行时获取泛型类型
*
* 原理:匿名子类的父类泛型信息,在.class文件的Signature属性中被保留,
* 可以通过 getGenericSuperclass() 运行时获取
*/
public abstract class MyTypeReference<T> {
private final Type type;
protected MyTypeReference() {
// 获取父类的泛型类型(this是匿名子类,父类是MyTypeReference<T>)
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof Class<?>) {
throw new IllegalArgumentException("MyTypeReference必须携带类型参数");
}
// 从ParameterizedType中取出实际类型参数
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
public static void main(String[] args) {
// 用法:创建匿名子类,泛型信息被保留在class文件里
MyTypeReference<List<String>> ref = new MyTypeReference<List<String>>() {};
System.out.println("Type: " + ref.getType());
// 输出:java.util.List<java.lang.String>
// 可以获取List的元素类型
if (ref.getType() instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) ref.getType();
System.out.println("容器类型: " + pt.getRawType()); // interface java.util.List
System.out.println("元素类型: " + pt.getActualTypeArguments()[0]); // class java.lang.String
}
// 这就是Jackson TypeReference的核心原理
// Jackson拿到这个Type信息,就知道要把JSON数组里的每个元素反序列化为String
}
}四、踩坑实录
坑1:JSON反序列化泛型集合,得到LinkedHashMap而不是业务对象
报错现象:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.dto.OrderDTO
at com.example.service.OrderService.processOrder(OrderService.java:78)
at com.example.service.OrderService.batchProcess(OrderService.java:45)根本原因:
调用方用objectMapper.readValue(json, List.class),Jackson在运行时只知道"需要一个List",不知道List里装什么,默认把JSON对象反序列化为LinkedHashMap。
具体解法:
// 使用TypeReference保留泛型信息
List<OrderDTO> orders = objectMapper.readValue(json, new TypeReference<List<OrderDTO>>() {});
// 或者用JavaType
JavaType type = objectMapper.getTypeFactory()
.constructCollectionType(List.class, OrderDTO.class);
List<OrderDTO> orders = objectMapper.readValue(json, type);坑2:泛型方法强转,ClassCastException延迟触发
报错现象:
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
at com.example.util.CacheUtils.get(CacheUtils.java:32) // 不是这里出的问题
at com.example.service.UserService.getUser(UserService.java:56) // 真正使用的地方根本原因:
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
Object value = cache.get(key);
return (T) value; // 字节码里这个转换被擦除,不检查
}
// 调用方
String name = CacheUtils.get("userId"); // key存的是Integer,但这里不报错
System.out.println(name.toUpperCase()); // 这里才报ClassCastException因为(T) value在字节码里被擦除,实际上是(Object) value(noop),只有在赋值给具体类型的变量后使用时,才触发真正的类型检查。
具体解法:
public static <T> T get(String key, Class<T> clazz) {
Object value = cache.get(key);
if (value != null && !clazz.isInstance(value)) {
throw new ClassCastException("缓存中[" + key + "]的类型为" +
value.getClass().getName() + ",无法转为" + clazz.getName());
}
return clazz.cast(value); // 用Class.cast做显式检查
}坑3:自定义泛型工具类,运行时拿不到类型
报错现象:
自己写了一个通用的Repository基类,想在基类里用T.class做一些操作,发现根本拿不到T的类型。
触发代码:
public abstract class BaseRepository<T> {
public List<T> findAll() {
// T.class // 编译错误,T是类型参数,不是类
// 无法在这里获取T的实际类型
return new ArrayList<>();
}
}具体解法:
// 方案1:通过构造函数传入Class<T>
public abstract class BaseRepository<T> {
private final Class<T> entityClass;
protected BaseRepository(Class<T> entityClass) {
this.entityClass = entityClass;
}
public List<T> findAll() {
// 可以用entityClass了
return entityManager.createQuery("from " + entityClass.getSimpleName(), entityClass).getResultList();
}
}
// 子类
public class OrderRepository extends BaseRepository<Order> {
public OrderRepository() {
super(Order.class);
}
}
// 方案2:通过反射从子类继承关系获取(Spring Data JPA用的方式)
@SuppressWarnings("unchecked")
protected Class<T> resolveEntityClass() {
Type superType = getClass().getGenericSuperclass();
if (superType instanceof ParameterizedType) {
return (Class<T>) ((ParameterizedType) superType).getActualTypeArguments()[0];
}
throw new IllegalStateException("无法解析实体类型");
}五、总结与延伸
泛型擦除是Java的一个历史设计决策。当初选择擦除主要是为了向后兼容JDK 1.4及以前的代码。这个决策带来了很多限制,也是Java饱受诟病的地方之一(Kotlin、Scala的reified泛型就不存在这个问题)。
把7个坑汇总一下:
| 坑点 | 现象 | 解法 |
|---|---|---|
| instanceof不能用泛型 | 编译错误 | 检查原始类型或元素类型 |
| 不能创建泛型数组 | 编译错误 | 用List或传入Class反射创建 |
| JSON反序列化泛型集合 | 得到LinkedHashMap | 用TypeReference |
| 泛型强转延迟报错 | ClassCastException在使用处 | 传入Class<T>做显式检查 |
| 静态方法不能用类的泛型 | 编译错误 | 静态方法定义自己的泛型参数 |
| 运行时拿不到泛型类型 | 反射返回null | 通过构造函数传入或匿名子类 |
| 重载方法泛型冲突 | 编译错误 | 改方法名或增加区分参数 |
最常见的是JSON反序列化这个坑,新人踩了之后往往很困惑。理解了TypeReference的原理——通过匿名子类的Signature属性保留泛型信息——就知道为什么它能解决这个问题了。
Java 17+有一些改进,但泛型擦除本身没有根本变化。如果你在意运行时泛型,Kotlin的inline fun <reified T>是个更优雅的方案,有机会单独说这个。
