一句话总结
Java 泛型是编译期检查,运行时通过类型擦除替换为原始类型(无界→Object,有界→上界),兼容旧代码。PECS 原则:Producer Extends(读)、Consumer Super(写)。桥接方法保持多态,匿名内部类可绕过擦除获取泛型信息。
初级理解
Java 泛型使用类型擦除(Type Erasure)实现,泛型信息只在编译期存在,编译后会被擦除,替换为原始类型(Raw Type)。这是为了兼容 JDK 5 之前的代码。
擦除规则:
1. 无限定的类型参数 <T> → 擦除为 Object
2. 有上界的类型参数 <T extends Number> → 擦除为上界 Number
3. 有多个上界的 <T extends A & B> → 擦除为第一个上界 A
// 编译前
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
// 编译后(等价于)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 编译器自动插入强制转型
由于类型擦除,以下代码无法通过编译(两个方法签名冲突):
// 编译错误:两个方法签名相同
public void print(List<String> list) {}
public void print(List<Integer> list) {}
中级深入
通配符和 PECS 原则:
PECS(Producer Extends, Consumer Super):从泛型集合读取数据用 ? extends T(生产者),向泛型集合写入数据用 ? super T(消费者)。
// Producer Extends:只能读,不能写
List<? extends Number> numbers = new ArrayList<Integer>();
Number n = numbers.get(0); // OK,读取
// numbers.add(1); // 编译错误,不能写入
// Consumer Super:只能写,读出来是 Object
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // OK,写入
Object obj = list.get(0); // 只能读到 Object
泛型数组为什么不能创建?由于类型擦除,数组在运行时知道自己的元素类型,但泛型信息已被擦除,两者冲突。例如 new ArrayList<String>[10] 是非法的。
高级拓展
桥接方法(Bridge Method):当子类实现泛型接口或继承泛型父类时,编译器会自动生成桥接方法来保持多态性。
// 源码
interface Comparator<T> {
int compare(T o1, T o2);
}
class StringLengthComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
// 编译后,StringLengthComparator 实际有两个方法:
// 1. public int compare(String o1, String o2) — 我们写的
// 2. public int compare(Object o1, Object o2) — 桥接方法
// { return compare((String)o1, (String)o2); }
如何绕过类型擦除获取泛型信息?虽然运行时泛型被擦除了,但可以通过反射获取某些场景下的泛型信息:
1. 子类继承泛型父类时,父类的泛型参数会被保留在字节码的 Signature 属性中
2. 通过 ParameterizedType 可以获取:
// 匿名内部类保留泛型信息
Type type = new TypeReference<List<String>>() {}.getType();
// type 可以获取到 List<String> 的完整泛型信息
面试加分项:能说出桥接方法的生成原理和 PECS 原则,说明你对泛型有深入理解。
实战场景
场景:泛型 DAO 基类
// 泛型 DAO 基类,减少重复代码
public class BaseDao<T, ID> {
@Autowired
private JdbcTemplate jdbcTemplate;
public T findById(ID id) {
// 通过反射获取泛型实际类型
Class<T> entityClass = getEntityClass();
return jdbcTemplate.queryForObject(
"SELECT * FROM " + entityClass.getSimpleName() + " WHERE id = ?",
new BeanPropertyRowMapper<>(entityClass), id);
}
private Class<T> getEntityClass() {
// 子类继承时保留泛型信息
return (Class<T>) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
}
}
// 使用:一行代码搞定
public class UserDao extends BaseDao<User, Long> {}
场景:PECS 实战 — 集合复制
// 从 src 读取(Producer),写入 dest(Consumer)
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(ints, nums); // Integer extends Number → OK
面试模拟
Q:为什么 Java 泛型用类型擦除而不是真泛型?
A:为了向后兼容。JDK 5 引入泛型时,已有大量无泛型代码。类型擦除让泛型代码和旧代码可以互操作,List<String> 和 List 在运行时是同一个类。代价是运行时丢失类型信息,无法用 instanceof 检查泛型类型。
Q:List<Object> 和 List<?> 有什么区别?
A:List<Object> 可以添加任意 Object,List<?> 只能添加 null(因为不知道具体类型)。List<Object> 是 List<?> 的子类型,但 List<Object> 不是 List<String> 的父类型(泛型没有协变性)。