1. 什么是泛型?
Java 泛型(Generics)是 JDK 5 中引入的一个重要特性,它允许在定义类、接口和方法时使用类型参数。泛型提供了一种在编译时检查类型安全的机制,并且所有的强制类型转换都是自动和隐式的,提高了代码的重用率和类型安全性。
2. Java 伪泛型的设计考量
Java 的泛型实现被称为"伪泛型",这是因为 Java 为了向后兼容早期没有泛型的版本,采用了类型擦除(Type Erasure)的实现方式。这种设计使得:
- 已有的非泛型代码可以在不修改的情况下继续运行
- 泛型代码可以与旧版本的 Java 代码共存
- JVM 层面不需要做大的改动就能支持泛型
3. 泛型的核心优势
3.1 类型安全
泛型在编译时进行类型检查,避免了运行时的 ClassCastException
:
// 没有泛型的情况
List list = new ArrayList();
list.add("Hello");
list.add(123); // 可以添加不同类型
String str = (String) list.get(1); // 运行时抛出 ClassCastException
// 使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // 编译时报错,类型不匹配
String str = stringList.get(0); // 无需强制转换,编译时保证类型安全
3.2 消除强制类型转换
使用泛型可以避免手动进行类型转换:
// 不使用泛型
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要强制类型转换
// 使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
String str = stringList.get(0); // 自动类型转换,无需强制转换
3.3 代码重用
泛型使我们能够编写适用于多种类型的通用代码。Java 标准库中的集合框架大量使用了泛型,这使得同一个集合类可以用于存储不同类型的数据:
// 同一个 ArrayList 类可以用于存储不同类型的数据
List<String> stringList = new ArrayList<>(); // 存储字符串
List<Integer> integerList = new ArrayList<>(); // 存储整数
List<File> fileList = new ArrayList<>(); // 存储文件对象
List<Map<String, Object>> mapList = new ArrayList<>(); // 存储映射对象
// 同一个 HashMap 类可以用于不同的键值对类型
Map<String, Integer> nameAgeMap = new HashMap<>(); // 字符串到整数的映射
Map<Integer, String> idNameMap = new HashMap<>(); // 整数到字符串的映射
Map<String, List<String>> categoryItemsMap = new HashMap<>(); // 字符串到字符串列表的映射
几乎所有 Java 集合框架都使用了泛型:
-
List<T>
、ArrayList<T>
、LinkedList<T>
-
Set<T>
、HashSet<T>
、TreeSet<T>
-
Map<K, V>
、HashMap<K, V>
、TreeMap<K, V>
-
Queue<T>
、Deque<T>
等
这使得开发者可以用一套通用的集合类来处理各种类型的对象,而不需要为每种类型都编写特定的集合类。
4. 泛型的基本语法
4.1 泛型类
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
使用时指定具体类型:
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String content = stringBox.getContent(); // 无需强制类型转换
4.2 泛型接口
public interface Container<T> {
void add(T item);
T get(int index);
}
实现泛型接口:
public class StringContainer implements Container<String> {
private List<String> items = new ArrayList<>();
@Override
public void add(String item) {
items.add(item);
}
@Override
public String get(int index) {
return items.get(index);
}
}
4.3 泛型方法
public class Util {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
调用泛型方法:
String[] names = {"Alice", "Bob", "Charlie"};
Util.swap(names, 0, 2); // 编译器会自动推断类型为String
5. 泛型的类型参数命名约定
- [E](file://D:\ideaProjects\crmeb_java\LICENSE) - Element(在集合中使用,如
List<E>
) -
K
- Key(表示映射中的键,如Map<K, V>
) -
V
- Value(表示映射中的值,如Map<K, V>
) -
N
- Number(表示数字类型) -
T
- Type(表示一般的类型) -
S, U, V
等 - 用于表示第二、第三、第四个类型参数
6. 通配符
6.1 上界通配符(? extends T)- 只读特性
上界通配符表示参数化类型可能是 T
或 T
的子类型。使用上界通配符时,我们只能从中读取数据,而不能写入数据(除了 null),因此具有只读特性:
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue(); // 可以读取数据
}
list.add(null); // 编译成功
// list.add(new Integer(1)); // 编译错误!不能添加元素
// list.add(new Double(2.0)); // 编译错误!不能添加元素
return sum;
}
// 使用示例
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5);
double sum1 = sumOfList(integers); // 正常工作
double sum2 = sumOfList(doubles); // 正常工作
6.2 下界通配符(? super T)- 只写特性
下界通配符表示参数化类型可能是 T
或 T
的父类型。使用下界通配符时,我们可以写入数据,但读取的数据类型只能是 Object,因此具有只写特性:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i); // 可以添加 Integer 或其子类型的元素
}
// Integer num = list.get(0); // 编译错误!只能获取 Object 类型
Object obj = list.get(0); // 只能获取 Object 类型
}
// 使用示例
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(integers); // 正常工作
addNumbers(numbers); // 正常工作
addNumbers(objects); // 正常工作
6.3 无界通配符(?)
表示未知类型:
public static void printList(List<?> list) {
for (Object elem : list) { //只能通过Object读取
System.out.print(elem + " ");
}
System.out.println();
// list.add(new Object()); // 编译错误!除了null都不能添加
list.add(null); // 只能添加null
}
6.4 PECS 原则(Producer Extends Consumer Super)
这是使用通配符的重要原则:
-
Producer(生产者)使用 extends:当你只需要从对象中读取数据时,使用
? extends T
-
Consumer(消费者)使用 super:当你只需要向对象中写入数据时,使用
? super T
// 拷贝方法示例
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) { // src 是生产者,使用 extends
dest.add(item); // dest 是消费者,使用 super
}
}
// 使用示例
List<String> src = Arrays.asList("a", "b", "c");
List<Object> dest = new ArrayList<>();
copy(src, dest); // 正常工作,String 是 Object 的子类型
7. 类型擦除 - Java 伪泛型的核心机制
Java 泛型在编译时会进行类型擦除,这意味着泛型信息不会保留到运行时。这是 Java 为了兼容旧版本而采用的设计决策。编译器会执行以下操作:
- 将泛型类型替换为它们的边界类型(通常是
Object
) - 在适当的地方插入类型转换代码
- 生成桥接方法以保持多态性
// 编译前
public class Box<T> {
private T content;
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
// 编译后(简化版,实际字节码)
public class Box {
private Object content;
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
}
由于类型擦除的存在,以下代码在运行时是无法区分的:
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 运行时两者类型相同
System.out.println(stringList.getClass() == integerList.getClass()); // true
8. 反射可绕过泛型限制
虽然泛型在编译时提供了类型安全,但通过反射可以在运行时绕过这些限制:
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// 通过反射绕过泛型类型检查
try {
Method addMethod = List.class.getDeclaredMethod("add", Object.class);
addMethod.invoke(stringList, 123); // 添加整数到String列表
System.out.println(stringList); // 输出: [Hello, 123]
// 但在使用时会遇到问题
for (String str : stringList) {
// 当遍历到整数123时会抛出ClassCastException
System.out.println(str.length());
}
} catch (Exception e) {
e.printStackTrace();
}
9. 泛型的限制
9.1 不能使用基本类型
由于类型擦除后泛型被替换为引用类型,所以不能直接使用基本类型:
// 错误示例
Box<int> intBox = new Box<int>(); // 编译错误
// 正确示例
Box<Integer> intBox = new Box<Integer>();
9.2 运行时无法获取泛型类型信息
// 错误示例
public static <T> void method() {
T obj = new T(); // 编译错误,因为运行时不知道T是什么类型
}
9.3 不能创建泛型数组
// 错误示例
T[] array = new T[10]; // 编译错误
// 可以使用以下方式替代
T[] array = (T[]) new Object[10];
9.4 不能实例化类型参数
// 错误示例
public class Box<T> {
private T instance = new T(); // 编译错误
}
10. 真泛型 vs 伪泛型
10.1 真泛型(如 C#)
- 泛型信息保留在运行时
- 可以在运行时获取泛型的实际类型
- 支持基本类型作为泛型参数
- 性能更好,因为避免了类型擦除和强制转换
10.2 Java 伪泛型的特点
- 为了向后兼容而采用类型擦除
- 运行时丢失泛型信息
- 不支持基本类型直接作为泛型参数
- 需要装箱/拆箱操作处理基本类型
11. 最佳实践
- 优先使用泛型:在定义集合和容器类时优先使用泛型
- 使用合适的通配符:根据实际需求选择合适的通配符
- 遵循 PECS 原则:Producer Extends Consumer Super
-
避免原始类型:尽量避免使用原始类型(如
List
而不是List<String>
) - 合理使用泛型方法:当类型参数只在方法级别使用时,使用泛型方法而非泛型类
- 理解类型擦除:了解类型擦除机制,避免在运行时依赖泛型类型信息
12. 总结
Java 泛型是现代 Java 开发中不可或缺的重要特性,它提供了编译时类型安全检查,减少了强制类型转换的需要,提高了代码的可读性和可维护性。虽然 Java 的泛型实现是"伪泛型",但这种设计确保了与早期 Java 版本的完全兼容性。
通过本文的介绍,你应该对 Java 泛型有了全面的了解,包括其基本语法、通配符的使用、类型擦除机制以及在实际开发中的应用。在日常开发中,合理使用泛型可以让你的代码更加安全、简洁和优雅。理解 Java 泛型的"伪泛型"本质有助于更好地使用这一特性,并避免常见的陷阱。