泛型
问题
Java 泛型是什么?类型擦除是怎么回事?? extends T 和 ? super T 有什么区别?
答案
泛型基础
泛型(Generics)允许在定义类、接口、方法时使用类型参数,在使用时指定具体类型,实现编译期类型安全:
// 不使用泛型:需要强制转换,运行时可能 ClassCastException
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要手动转型
// 使用泛型:编译期检查类型安全
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误
String s = list.get(0); // 无需转型
泛型类、接口、方法
// 泛型类:类名后声明类型参数
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// 泛型接口
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
// 泛型方法:返回类型前声明类型参数 <T>
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 调用时可以显式指定类型,也可以让编译器自动推断
String first = GenericExamples.<String>getFirst(names); // 显式
String first = getFirst(names); // 推断(推荐)
类型擦除(Type Erasure)
Java 泛型是通过类型擦除实现的——编译后泛型信息被擦除,替换为原始类型(无界用 Object,有界用上界):
// 源码
public class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 编译后(擦除后)
public class Box {
private Object value;
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
// 有界泛型 <T extends Comparable<T>> 擦除为 Comparable
类型擦除的后果:
// 1. 不能用基本类型作为泛型参数
// List<int> list; // 编译错误,只能用 List<Integer>
// 2. 不能创建泛型数组
// T[] arr = new T[10]; // 编译错误
T[] arr = (T[]) new Object[10]; // 只能这样(有 unchecked 警告)
// 3. 不能用 instanceof 检查泛型类型
// if (obj instanceof List<String>) // 编译错误
if (obj instanceof List<?>) {} // 只能用无界通配符
// 4. 运行时泛型信息丢失
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 运行时两者的 Class 对象相同
System.out.println(strings.getClass() == integers.getClass()); // true
为了向后兼容。Java 5 引入泛型时,需要与大量已存在的非泛型代码(Java 1.4 及之前)兼容。类型擦除使得泛型代码编译后的字节码与非泛型代码一致。
通配符
无界通配符 <?>
表示未知类型,只能读不能写(写入无法保证类型安全):
// 打印任意类型的 List
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
// list.add("test"); // 编译错误:不知道实际类型,无法添加
}
上界通配符 <? extends T>(协变)
表示 T 或 T 的子类型,只能读取,不能写入:
// 可以接受 List<Integer>、List<Double> 等
public double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue(); // 读取:安全,都是 Number
}
// list.add(1); // 编译错误:不知道具体是哪个子类型
return total;
}
下界通配符 <? super T>(逆变)
表示 T 或 T 的父类型,只能写入,读取只能作为 Object:
// 可以接受 List<Integer>、List<Number>、List<Object>
public void addIntegers(List<? super Integer> list) {
list.add(1); // 写入 Integer:安全
list.add(2);
// Integer n = list.get(0); // 编译错误:只能读取为 Object
Object obj = list.get(0); // 读取为 Object
}
PECS 原则
Producer Extends, Consumer Super:
- 生产者(提供数据)用
extends:从集合中读取元素 - 消费者(接收数据)用
super:向集合中写入元素
// Collections.copy 的签名是 PECS 的经典示例
public static <T> void copy(
List<? super T> dest, // 消费者:写入数据 → super
List<? extends T> src // 生产者:读取数据 → extends
) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
// 使用示例
List<Number> numbers = new ArrayList<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
Collections.copy(numbers, integers); // Number super Integer ✅, Integer extends Number ✅
泛型的约束与边界
// 多重边界:T 必须同时满足多个约束(类在前,接口在后)
public <T extends Comparable<T> & Serializable> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// 递归类型边界:常见于 Comparable 的定义
public class Enum<E extends Enum<E>> { } // Java 枚举的实际声明
常见面试问题
Q1: 什么是类型擦除?它带来了哪些限制?
答案:
类型擦除是 Java 泛型的实现方式——编译时检查泛型类型安全,编译后擦除泛型信息,替换为原始类型。限制包括:
- 不能用基本类型(
int)作为类型参数 - 不能创建泛型类型的实例(
new T())或数组(new T[]) - 不能用
instanceof检查参数化类型 - 不能创建参数化类型的数组(
new List<String>[10]) - 静态字段不能使用泛型类的类型参数
Q2: List<Object> 和 List<?> 的区别?
答案:
| 维度 | List<Object> | List<?> |
|---|---|---|
| 含义 | 确定类型为 Object 的列表 | 未知类型的列表 |
| 写入 | 可以添加任意对象 | 不能添加(除了 null) |
| 读取 | 读出 Object | 读出 Object |
| 赋值兼容 | List<String> 不能赋值给 List<Object> | List<String> 可以赋值给 List<?> |
List<Object> 不是 List<String> 的父类型(泛型不协变),但 List<?> 是所有 List<X> 的通配符父类型。
Q3: <T extends Comparable<T>> 是什么意思?
答案:
这是递归类型边界,表示类型参数 T 必须实现 Comparable<T> 接口,即 T 可以与自己比较。这常用于排序、查找最值等需要比较的场景:
public static <T extends Comparable<T>> T max(List<T> list) {
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
Q4: 泛型方法和泛型类中的类型参数有什么区别?
答案:
- 泛型类的类型参数:在类实例化时确定,整个类共享一个类型
- 泛型方法的类型参数:在方法调用时确定,独立于类的类型参数
public class Container<T> {
private T value;
// 泛型方法的 <E> 与类的 <T> 无关
public <E> E transform(Function<T, E> mapper) {
return mapper.apply(value);
}
}
泛型方法更灵活,且静态方法不能使用类的类型参数(因为类型参数属于实例),只能声明自己的泛型参数。
Q5: 如何在运行时获取泛型的实际类型?
答案:
虽然有类型擦除,但有几种方式可以在运行时获取泛型信息:
-
通过子类化保留:匿名内部类或子类继承参数化父类时,泛型信息保留在 Class 的 metadata 中
// TypeReference 模式(Jackson、Gson 等库使用)
Type type = new TypeReference<List<String>>() {}.getType();
// 获取到 ParameterizedType: List<String> -
通过字段的声明类型:
Field field = MyClass.class.getDeclaredField("list");
ParameterizedType type = (ParameterizedType) field.getGenericType();
Type[] args = type.getActualTypeArguments(); // [String.class]
Q6: Arrays.asList() 返回的 List 添加元素会怎样?
答案:
Arrays.asList() 返回的是 java.util.Arrays.ArrayList(内部类),它基于固定长度的数组,不支持 add 和 remove,调用会抛 UnsupportedOperationException。但可以修改已有元素(set):
List<String> list = Arrays.asList("a", "b", "c");
list.set(0, "A"); // OK
list.add("d"); // UnsupportedOperationException
// 创建可修改的 ArrayList
List<String> mutable = new ArrayList<>(Arrays.asList("a", "b", "c"));
// 或 JDK 9+
List<String> immutable = List.of("a", "b", "c"); // 完全不可变