Appearance
泛型
为什么需要泛型?
在没有泛型的 Java 1.4 时代,集合操作依赖 Object 类型存储和强制转型:
java
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过,但逻辑上可能是个 bug
String s = (String) list.get(0); // 必须强转,类型不安全这带来三个问题:
- 类型不安全:
list.add(123)不会报错,运行时才会抛出ClassCastException - 代码冗余:每次从集合取出元素都要写
(String)强转 - 可读性差:看代码无法立即知道集合里存的是什么类型
泛型(JDK 1.5 引入)在编译期就解决了这些问题:
java
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译直接报错
String s = list.get(0); // 无需强转,编译器保证类型安全泛型的核心价值:将运行时的类型错误提前到编译期发现。
泛型的基本概念
类型参数化
泛型的本质是类型参数化——把类型当作参数传递,让同一段代码适配多种数据类型。
java
// 没有泛型——每种类型写一个类
class IntegerBox { private Integer value; ... }
class StringBox { private String value; ... }
// 有泛型——一个类通吃所有类型
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}类型参数命名约定
| 字母 | 含义 | 典型场景 |
|---|---|---|
E | Element | 集合元素(List<E>) |
K | Key | 键(Map<K,V>) |
V | Value | 值(Map<K,V>) |
T | Type | 通用类型 |
S, U | 第二、第三类型参数 | 多参数场景 |
? | 通配符 | 未知类型 |
泛型类
在类名后声明类型参数,类内部即可使用:
java
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 void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
}
// 使用
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey(); // 无需强转
Integer value = pair.getValue();泛型类中的静态成员
静态成员不能引用类的类型参数,因为类型参数在实例化时才确定,而静态成员属于类本身:
java
public class Container<T> {
// private static T value; // 编译错误!
// 静态方法如果想用泛型,必须声明自己的类型参数
public static <U> U identity(U value) {
return value;
}
}泛型接口
java
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
List<T> findAll();
}
// 实现时指定具体类型
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) { /* ... */ return null; }
@Override
public void save(User entity) { /* ... */ }
@Override
public List<User> findAll() { /* ... */ return null; }
}
// 实现类也可以保留泛型
public class GenericRepository<T, ID> implements Repository<T, ID> {
// 实现方法...
}泛型方法
方法可以独立于类声明自己的类型参数:
java
public class Utils {
// 泛型方法:类型参数声明在返回值前
public static <T> T getMiddle(T... items) {
return items[items.length / 2];
}
// 类型参数带边界
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// 调用
String middle = Utils.<String>getMiddle("a", "b", "c"); // 显式指定
String middle2 = Utils.getMiddle("a", "b", "c"); // 类型推断类型擦除
擦除原理
Java 泛型是编译期概念,运行时泛型信息会被"擦除"。这是为了向后兼容——JVM 不需要理解泛型,老代码可以直接运行在新 JVM 上。
java
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时,两者都是普通 ArrayList
System.out.println(strList.getClass() == intList.getClass()); // true
System.out.println(strList.getClass()); // class java.util.ArrayList擦除规则
| 泛型形式 | 擦除结果 |
|---|---|
List<T>(无边界) | List(T 擦除为 Object) |
List<T extends Number> | List(T 擦除为 Number) |
List<?> | List |
java
// 编译后等价于:
public class Box {
private Object value; // T 被擦除为 Object
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}桥接方法(Bridge Method)
类型擦除后,编译器会生成桥接方法保证多态性:
java
class Node<T> {
private T data;
public void setData(T data) { this.data = data; }
}
class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) { /* ... */ }
// 编译器自动生成桥接方法:
// public void setData(Object data) { setData((Integer) data); }
}这是反编译后能看到 volatile / bridge 标记的方法的原因。
泛型边界
上界通配符 ? extends T
限制类型必须是 T 或其子类。只能读,不能写(除了 null):
java
// 能接受 List<Integer>、List<Double>、List<Number> 等
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue(); // 可以读,当作 Number 处理
}
// numbers.add(10); // 编译错误!不能写
return total;
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 7.5下界通配符 ? super T
限制类型必须是 T 或其父类。可以写,读出来是 Object:
java
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // 可以写 Integer
}
// Integer num = list.get(0); // 编译错误!读出来是 Object
}
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // OK,Number 是 Integer 的父类
List<Object> objects = new ArrayList<>();
addNumbers(objects); // OK,Object 是 Integer 的父类PECS 原则
Producer Extends, Consumer Super
- 如果只需要读取数据(生产者),用
? extends T - 如果只需要写入数据(消费者),用
? super T - 如果既读又写,直接用具体类型
<T>
java
// Collections.copy 的签名完美体现了 PECS:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// src 是生产者(读取),用 extends
// dest 是消费者(写入),用 super
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}无界通配符 <?>
表示未知类型,等同于 <? extends Object>:
java
// 当方法只用到 Object 的方法时,用 <?> 比 <T> 语义更清晰
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}泛型的限制
不能使用基本类型
泛型参数必须是引用类型,不能是 int、double 等基本类型:
java
// List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // 正确,用包装类原因:类型擦除后 T 被替换为 Object,Object 不能存储基本类型值。
不能创建泛型数组
java
// T[] array = new T[10]; // 编译错误
// List<String>[] arr = new List<String>[10]; // 编译错误原因:数组在运行时保留元素类型信息,而泛型在运行时被擦除,两者冲突。如果允许,会破坏类型安全:
java
// 假设允许创建泛型数组:
// List<String>[] strArr = new List<String>[10];
// Object[] objArr = strArr; // 数组协变
// objArr[0] = new ArrayList<Integer>(); // 能通过数组检查
// String s = strArr[0].get(0); // ClassCastException!变通方案:使用 ArrayList 代替数组,或创建通配符数组后强转:
java
List<String>[] arr = (List<String>[]) new ArrayList[10]; // 警告,但可行不能 instanceof 泛型类型
java
// if (list instanceof List<String>) {} // 编译错误
if (list instanceof List) {} // 只能检查原始类型原因:运行时泛型信息已被擦除,无法区分 List<String> 和 List<Integer>。
不能实例化类型参数
java
public <T> T create() {
// return new T(); // 编译错误
return null;
}原因:类型擦除后 T 变成 Object,并且编译器不知道 T 是否有无参构造器。需要使用反射或 Supplier:
java
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
public <T> T create(Supplier<T> supplier) {
return supplier.get();
}不能抛出泛型异常
java
// class MyException<T> extends Exception {} // 编译错误
// catch (T e) {} // 编译错误不能在静态上下文中使用类型参数
java
class Foo<T> {
// static T value; // 编译错误
// static void method(T t) {} // 编译错误
}泛型与继承关系
List<String> 不是 List<Object> 的子类型。即使 String 是 Object 的子类,泛型集合也不具备协变性:
java
List<String> strList = new ArrayList<>();
// List<Object> objList = strList; // 编译错误!这是为了类型安全。如果允许,就会出现:
java
// 假设允许
// List<Object> objList = strList;
// objList.add(123); // 把一个 Integer 塞进了 String 列表
// String s = strList.get(0); // 运行时 ClassCastException常见面试题
Q1: Java 泛型是如何实现的?什么是类型擦除?
Java 泛型通过类型擦除实现,泛型信息只存在于编译期。编译后,所有类型参数被替换为边界类型(无边界则为 Object),并插入必要的强制转型代码。这是为了向后兼容老版本 JVM。
Q2: List<?>、List<Object>、List 有什么区别?
| 声明 | 含义 | 可以 add 吗? |
|---|---|---|
List | 原始类型,无泛型检查 | 可以 add 任何对象,但编译警告 |
List<Object> | 明确存放 Object | 可以 add 任何对象 |
List<?> | 未知类型,只读 | 只能 add null |
List<?> 和 List<Object> 不是同一个类型,List<?> 是只读的。
Q3: PECS 原则是什么?
Producer Extends, Consumer Super。如果从集合读取数据,使用 ? extends T;如果向集合写入数据,使用 ? super T;如果既读又写,直接用 <T>。
Q4: 为什么不能创建泛型数组?
数组在运行时保留元素类型(协变),泛型在运行时被擦除。如果允许泛型数组,会导致数组的类型检查被绕过,进而引发运行时的 ClassCastException。
Q5: 桥接方法是什么?为什么需要它?
类型擦除后,子类重写父类泛型方法时,方法签名可能不匹配。编译器自动生成一个桥接方法(类型擦除后的签名),在桥接方法内部调用子类实际方法,保证多态性正常工作。
Q6: 泛型方法中的 <T> 放在什么位置?
放在返回值类型之前。例如 public static <T> T getValue(T t)。
Q7: 为什么不能 catch (T e) 或 throw new T()?
JVM 需要知道异常的具体类型来匹配 catch 块。类型擦除后 T 变成 Object,JVM 无法判断实际类型。同时泛型类不能继承 Throwable。
