泛型
泛型(Generics)是 JDK 5 引入的重要特性,它提供了编译时类型安全检测机制,允许在编译时检测到非法类型。理解泛型的本质和类型擦除机制,是 Java 面试的高频考点。
一、泛型概述
1.1 什么是泛型
泛型是一种参数化类型,允许在定义类、接口、方法时使用类型参数,在使用时再指定具体类型。
// 没有泛型(JDK 5 之前)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制转换
// 使用泛型(JDK 5+)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 无需转换,类型安全1.2 泛型的优势
| 优势 | 说明 |
|---|---|
| 类型安全 | 编译时检查类型,避免运行时 ClassCastException |
| 消除强制转换 | 编译器自动插入类型转换代码 |
| 代码复用 | 同一套代码可以用于多种类型 |
| 可读性 | 代码意图更清晰 |
// 没有 泛型时的问题
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过,运行时出错
String s = (String) list.get(1); // ClassCastException
// 使用 泛型后
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误,提前发现
String s = list.get(0); // 无需转换二、泛型类与泛型接口
2.1 泛型类
// 泛型类定义
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer i = intBox.get();
// 常见的类型参数命名
T - Type(类型)
E - Element(元素,常用于集合)
K - Key(键)
V - Value(值)
N - Number(数值)
R - Result(结果)2.2 泛型接口
// 泛型接口定义
public interface Generator<T> {
T generate();
}
// 实现方式1:指定具体类型
public class StringGenerator implements Generator<String> {
@Override
public String generate() {
return "Hello";
}
}
// 实现方式2:保留泛型参数
public class GenericGenerator<T> implements Generator<T> {
@Override
public T generate() {
return null;
}
}2.3 多个类型参数
// 多个类型参数
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; }
}
// 使用
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey();
Integer value = pair.getValue();
// JDK 7+ 菱形语法(类型推断)
Pair<String, Integer> pair = new Pair<>("age", 25);三、泛型方法
3.1 泛型方法定义
泛型方法可以在普通类或泛型类中定义,类型参数放在返回值前面。
// 泛型方法
public <T> T getFirst(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
// 静态泛型方法
public static <T> void print(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
// 使用
String first = getFirst(Arrays.asList("a", "b", "c"));
Integer num = getFirst(Arrays.asList(1, 2, 3));
// 类型推断
print(new String[]{"a", "b", "c"});
print(new Integer[]{1, 2, 3});
// 显式指定类型
this.<String>print(new String[]{"a", "b", "c"});3.2 泛型方法 vs 泛型类
public class Demo<T> {
// 使用类的类型参数 T
public T method1(T t) {
return t;
}
// 泛型方法,使用自己的类型参数 E
public <E> E method2(E e) {
return e;
}
// 静态方法不能使用类的类型参数
// public static T method3(T t) { } // 编译错误
// 静态方法可以是泛型方法
public static <E> E method4(E e) {
return e;
}
}3.3 可变参数与泛型
// 泛型可变参数
@SafeVarargs // 抑制堆污染警告
public static <T> List<T> asList(T... elements) {
List<T> list = new ArrayList<>();
for (T element : elements) {
list.add(element);
}
return list;
}
// 使用
List<String> list = asList("a", "b", "c");
List<Integer> nums = asList(1, 2, 3);四、类型参数限定
4.1 上界限定(extends)
使用 extends 关键字限定类型参数必须是某个类或接口的子类。
// 单一上界
public <T extends Number> double sum(List<T> list) {
double total = 0;
for (T num : list) {
total += num.doubleValue();
}
return total;
}
// 多个上界(类在前,接口在后)
public <T extends Number & Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) return null;
T maxValue = list.get(0);
for (T item : list) {
if (item.compareTo(maxValue) > 0) {
maxValue = item;
}
}
return maxValue;
}
// 泛型类上界
public class NumberBox<T extends Number> {
private T value;
// ...
}
// 使用
NumberBox<Integer> intBox = new NumberBox<>();
NumberBox<Double> doubleBox = new NumberBox<>();
// NumberBox<String> stringBox = new NumberBox<>(); // 编译错误4.2 下界限定(super)
下界限定只能用于通配符,不能用于类型参数定义。
// 下界通配符
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 使用
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // ✅ Integer 是 Number 的子类
List<Object> objects = new ArrayList<>();
addNumbers(objects); // ✅ Integer 是 Object 的子类
List<Double> doubles = new ArrayList<>();
// addNumbers(doubles); // ❌ 编译错误五、通配符
5.1 三种通配符
| 通配符 | 名称 | 含义 |
|---|---|---|
<?> | 无界通配符 | 任意类型 |
<? extends T> | 上界通配符 | T 或 T 的子类 |
<? super T> | 下界通配符 | T 或 T 的父类 |
5.2 无界通配符
// 无界通配符
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
// list.add("hello"); // 编译错误:不能添加元素(除了 null)
list.add(null); // 唯一可以添加的值
}
// 使用
printList(Arrays.asList("a", "b", "c"));
printList(Arrays.asList(1, 2, 3));
// List<?> 与 List<Object> 的区别
List<?> list1; // 可以接受任何类型的 List
List<Object> list2; // 只能接受 List<Object>
List<String> strings = new ArrayList<>();
list1 = strings; // ✅
// list2 = strings; // ❌ 编译错误5.3 上界通配符(PECS - Producer Extends)
// 上界通配符:适合读取(生产者)
public double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) { // 可以读取为 Number
total += num.doubleValue();
}
// list.add(1); // 编译错误:不能添加元素
return total;
}
// 使用
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0);
System.out.println(sum(integers)); // 6.0
System.out.println(sum(doubles)); // 6.05.4 下界通配符(PECS - Consumer Super)
// 下界通配符:适合写入(消费者)
public void addIntegers(List<? super Integer> list) {
list.add(1); // ✅ 可以添加 Integer
list.add(2);
list.add(3);
// Integer i = list.get(0); // 编译错误:读取类型不确定
Object obj = list.get(0); // 只能读取为 Object
}
// 使用
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(numbers);
addIntegers(objects);5.5 PECS 原则
PECS: Producer Extends, Consumer Super
┌────────────────────────────────────────────────┐
│ PECS 原则 │
├────────────────────────────────────────────────┤
│ │
│ 如果需要从集合中读取数据(生产者) │
│ → 使用 ? extends T │
│ │
│ 如果需要向集合中写入数据(消费者) │
│ → 使用 ? super T │
│ │
│ 如果既读又写 │
│ → 不使用通配符,直接使用具体类型 T │
│ │
└────────────────────────────────────────────────┘// 经典示例:Collections.copy
public static <T> void copy(
List<? super T> dest, // 消费者:写入
List<? extends T> src // 生产者:读取
) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
// 使用
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src); // ✅ dest 消费,src 生产六、类型擦除
6.1 什么是类型擦除
类型擦除是 Java 泛型的实现机制:泛型信息只存在于编译期,运行时被擦除。
// 源代码
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 编译后(字节码)
List strings = new ArrayList();
List integers = new ArrayList();
// 运行时类型相同
System.out.println(strings.getClass() == integers.getClass()); // true6.2 擦除规则
| 泛型类型 | 擦除后类型 |
|---|---|
<T> | Object |
<T extends Number> | Number |
<T extends Number & Comparable> | 第一个上界 Number |
List<String> | List |
T[] | 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; }
}
// 有上界的泛型
public class NumberBox<T extends Number> {
private T value;
}
// 擦除后
public class NumberBox {
private Number value;
}6.3 桥接方法
当泛型类继承或实现泛型接口时,编译器会生成桥接方法来保证多态正确性。
// 泛型接口
public interface Comparable<T> {
int compareTo(T o);
}
// 实现
public class String implements Comparable<String> {
@Override
public int compareTo(String o) {
return this.compareTo(o);
}
}
// 编译器生成的桥接方法
public class String implements Comparable<String> {
// 原方法
public int compareTo(String o) { ... }
// 桥接方法(类型擦除后的签名)
public int compareTo(Object o) {
return compareTo((String) o);
}
}6.4 类型擦除的影响
影响1:不能用基本类型
// ❌ 编译错误
List<int> list = new ArrayList<>();
// ✅ 使用包装类
List<Integer> list = new ArrayList<>();影响2:不能创建泛型数组
// ❌ 编译错误
T[] array = new T[10];
// ✅ 使用 Object 数组或反射
Object[] array = new Object[10];
T[] array = (T[]) Array.newInstance(clazz, 10);影响3:不能实例化类型参数
// ❌ 编译错误
public <T> T create() {
return new T();
}
// ✅ 使用反射
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}影响4:不能重载具有相同擦除签名的方法
// ❌ 编译错误:擦除后签名相同
public void print(List<String> list) { }
public void print(List<Integer> list) { }
// 擦除后都是:
// public void print(List list) { }影响5:运行时类型检查不准确
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 运行时类型相同
System.out.println(strings.getClass() == integers.getClass()); // true
// instanceof 不能用于泛型
if (strings instanceof List<String>) { } // 编译错误
if (strings instanceof List) { } // ✅ 正确
// 可以使用 Class 对象
if (strings instanceof ArrayList) { } // ✅ 正确七、泛型与数组
7.1 泛型数组的限制
// ❌ 不能创建泛型数组
T[] array = new T[10];
List<String>[] array = new List<String>[10];
// ✅ 可以声明泛型数组类型
T[] array; // 声明:允许
List<String>[] array; // 声明:允许
// ✅ 创建原始类型数组后强制转换(有警告)
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[10];7.2 为什么禁止创建泛型数组?
// 如果允许创建泛型数组会导致类型安全问题
List<String>[] stringLists = new List<String>[1]; // 假设允许
Object[] objects = stringLists; // 数组协变
objects[0] = new ArrayList<Integer>(); // 运行时不会报错
String s = stringLists[0].get(0); // ClassCastException!
// Java 通过禁止创建泛型数组来避免这个问题7.3 替代方案
// 方案1:使用 List 代替数组
List<List<String>> listOfLists = new ArrayList<>();
// 方案2:使用反射创建数组
@SuppressWarnings("unchecked")
public <T> T[] createArray(Class<T> componentType, int size) {
return (T[]) Array.newInstance(componentType, size);
}
// 方案3:使用 Object 数组并手动转换
private Object[] array = new Object[10];
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index];
}八、泛型最佳实践
8.1 命名规范
// ✅ 推荐:使用单个大写字母
T - Type
E - Element
K - Key
V - Value
N - Number
R - Result
// ✅ 多个类型参数
public class Pair<K, V> { }
// ❌ 不推荐:使用小写或单词
public class Box<type> { }
public class Box<TypeOfElement> { }8.2 优先使用泛型方法
// ❌ 不推荐:在类级别定义泛型,但只在方法中使用
public class Utils<T> {
public T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
}
// ✅ 推荐:使用泛型方法
public class Utils {
public <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
}8.3 使用有界类型参数增加灵活性
// ❌ 不够灵活
public double sum(List<Number> list) { ... }
// ✅ 更灵活
public double sum(List<? extends Number> list) { ... }8.4 避免原始类型
// ❌ 原始类型:失去类型安全
List list = new ArrayList();
// ✅ 使用泛型
List<String> list = new ArrayList<>();
// ✅ 如果类型不确定,使用通配符
List<?> list = new ArrayList<String>();九、常见面试题
Q1: Java 泛型的实现原理是什么?
A: Java 泛型通过类型擦除实现。泛型信息只在编译期存在,运行时会被擦除为原始类型。这是为了兼容 JDK 5 之前的代码。
Q2: 什么是类型擦除?有什么影响?
A: 类型擦除是指编译器在编译时将泛型类型替换为原始类型或上界类型的过程。影响包括:
- 不能用基本类型作为类型参数
- 不能创建泛型数组
- 不能实例化类型参数
- 运行时类型检查不准确
Q3: List<?> 和 List<Object> 有什么区别?
A:
| 对比项 | List<?> | List<Object> |
|---|---|---|
| 可以接受 | 任意类型的 List | 只能是 List<Object> |
| 类型安全 | 读取不安全,写入不安全 | 读写都安全 |
| 添加元素 | 只能添加 null | 可以添加任意 Object |
接受 List<String> | ✅ | ❌ |
List<?> list1;
List<Object> list2;
List<String> strings = new ArrayList<>();
list1 = strings; // ✅
// list2 = strings; // ❌ 编译错误Q4: 什么是 PECS 原则?
A: PECS: Producer Extends, Consumer Super
- 从集合读取数据(生产者)→ 用
? extends T - 向集合写入数据(消费者)→ 用
? super T - 既读又写 → 用具体类型
T
Q5: 为什么不能创建泛型数组?
A: 因为数组的协变性会导致类型安全问题。如果允许创建泛型数组,可能将 List<Integer> 放入 List<String>[] 中,运行时不会报错,但取数据时会抛出 ClassCastException。
Q6: 下面代码输出什么?
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
List<?> list2 = list;
// list2.add(2); // 编译错误
System.out.println(list2.get(0)); // 输出什么?
}A: 输出 1。List<?> 可以读取元素,但读取的类型是 Object。不能添加非 null 元素。
Q7: 如何获取泛型的实际类型?
A: 通过反射可以获取泛型信息(保留在类、字段、方法的签名中):
public class Test {
private List<String> list;
public static void main(String[] args) throws Exception {
Field field = Test.class.getDeclaredField("list");
ParameterizedType type = (ParameterizedType) field.getGenericType();
System.out.println(type.getActualTypeArguments()[0]); // class java.lang.String
}
}十、总结
| 概念 | 核心要点 | 面试关键词 |
|---|---|---|
| 泛型本质 | 参数化类型,编译时类型安全 | 编译期检查 |
| 类型擦除 | 运行时泛型信息被擦除 | 原始类型、桥接方法 |
| 上界通配符 | ? extends T,适合读取 | PECS - Producer |
| 下界通配符 | ? super T,适合写入 | PECS - Consumer |
| 泛型限制 | 不能用基本类型、不能创建数组、不能实例化 | 擦除的影响 |
最后更新:2026年3月2日