泛型
- 为什么需要泛型?
- 泛型基础
- 类型边界
- 通配符(Wildcard)
-
- 为什么需要通配符?
- [无界通配符 `<?>`](#无界通配符
<?>) - [上界通配符 `<? extends T>`](#上界通配符
<? extends T>) - [下界通配符 `<? super T>`](#下界通配符
<? super T>) - [PECS 完整记忆法](#PECS 完整记忆法)
- 类型擦除
- 泛型在实际开发中的最佳实践
-
- 优先使用泛型,避免原始类型
- 使用泛型方法替代通配符(当类型参数有关联时)
- [复杂场景应用:构建类型安全的 Builder 模式](#复杂场景应用:构建类型安全的 Builder 模式)
- 常见陷阱与注意事项
在Java开发中,泛型(Generics)是一个看似简单却蕴含深意的特性。自JDK 5引入以来,它已经成为Java类型系统的重要组成部分。
为什么需要泛型?
泛型出现之前的问题
在泛型出现之前,Java集合类(如ArrayList)存储的是Object类型,这意味着可以向集合中添加任何类型的对象。
java
List list = new ArrayList();
list.add("Hello");
list.add(123); // 可以添加Integer,但容易引发问题
// 取出时需要强制转换
String str = (String) list.get(0); // 没问题
String error = (String) list.get(1); // 运行时抛出 ClassCastException
这段代码在编译时完全正常,但在运行时却会崩溃。问题的根源在于:编译器无法知道集合中元素的实际类型,类型安全只能靠程序员手动保证。
泛型的解决方案
泛型提供了编译时类型安全检查机制,在定义类、接口、方法时可以指定类型参数。
java
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误:不兼容的类型
String str = list.get(0); // 无需强制转换
通过泛型,类型错误在编译阶段就被发现了,代码也更加清晰易读。
泛型基础
泛型类
定义一个泛型类,使用尖括号<>声明类型参数,通常使用单个大写字母:
| 类型参数 | 含义 | 使用场景 |
|---|---|---|
T |
Type | 表示任意类型,最常用。当只有一个类型参数时,通常使用T |
E |
Element | 表示集合中的元素类型,如List<E>、Set<E> |
K |
Key | 表示映射中的键类型,如Map<K, V> |
V |
Value | 表示映射中的值类型,如Map<K, V> |
N |
Number | 表示数值类型 |
S, U, V |
第二、三、四个类型参数 | 当需要多个类型参数时的扩展 |
R |
Return Type | 表示方法的返回类型(常见于函数式编程) |
java
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<String>();
//Box<String> stringBox = new Box<>();省略后面的String也可以
stringBox.set("Hello");
String value = stringBox.get(); // 类型安全,无需转换
Box<Integer> intBox = new Box<>();//Box<Integer> intBox = new Box<Integer>();
intBox.set(123);
}
}
泛型接口
接口也可以声明泛型,最典型的例子就是List、Set、Map等集合接口:
java
public interface Generator<T> {
T next();
}
public class NumberGenerator implements Generator<Integer> {
private int count = 0;
@Override
public Integer next() {
return count++;
}
}
泛型方法
泛型方法可以定义在普通类中,也可以定义在泛型类中。类型参数放在返回类型之前:
java
public class GenericMethodExample {
// 泛型方法,定义类型参数 T
public static <T> T getMiddle(T... arr) {
return arr[arr.length / 2];
}
public static void main(String[] args) {
String middleStr = getMiddle("a", "b", "c"); // T 被推断为 String
Integer middleInt = getMiddle(1, 2, 3, 4); // T 被推断为 Integer
}
}
类型边界
有时我们需要限制类型参数的范围,比如只允许某个类的子类。这时可以使用extends关键字定义上界:
java
// 只允许 Number 及其子类
public class NumberBox<T extends Number> {
private T number;
public double doubleValue() {
return number.doubleValue(); // 可以调用 Number 类的方法
}
public void set(T number) {
this.number = number;
}
}
// 使用
NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<Double> doubleBox = new NumberBox<>(); // OK
// NumberBox<String> strBox = new NumberBox<>(); // 编译错误
多个边界
Java支持多个边界,使用&连接:
java
// T 必须同时是 Serializable 和 Comparable 的子类型
public class MultiBound<T extends Serializable & Comparable<T>> {
// ...
}
通配符(Wildcard)
通配符?是泛型中一个非常强大的概念,它解决了泛型类型之间的协变和逆变问题。
为什么需要通配符?
先看一个常见的困惑:List<String> 是 List<Object> 的子类型吗?
java
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!
答案是不是 。如果允许这样做,我们就可以向strings中添加非String类型的对象,破坏了类型安全。
那么,如何表示一个可以接受任何List的方法呢?这就引出了无界通配符。
无界通配符 <?>
java
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 可以传入任何类型的 List
List<String> names = Arrays.asList("Alice", "Bob");
List<Integer> numbers = Arrays.asList(1, 2, 3);
printList(names); // OK
printList(numbers); // OK
注意:List<?> 是只读的(除了添加null),因为你不知道具体的类型。
上界通配符 <? extends T>
需要从集合中读取元素时,使用上界通配符。它表示类型是T或T的子类。
java
public double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue(); // 可以安全地读取为 Number
}
return sum;
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2);
sumOfList(ints); // OK
sumOfList(doubles); // OK
PECS原则(Producer Extends, Consumer Super) :如果需要从集合中读取数据(生产者),使用extends。
下界通配符 <? super T>
需要向集合中写入元素时,使用下界通配符。它表示类型是T或T的超类。
java
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
// 可以安全地添加 Integer
}
// 使用
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK
// List<Integer> ints = new ArrayList<>(); // 也能用,但注意语义
PECS原则 :如果需要向集合中写入数据(消费者),使用super。
PECS 完整记忆法
PECS: Producer Extends, Consumer Super
- Producer (生产者) :如果你从一个数据结构中获取元素,它是生产者,使用
<? extends T> - Consumer (消费者) :如果你向一个数据结构中放入元素,它是消费者,使用
<? super T>
java
// 典型例子:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// src 是生产者,dest 是消费者
}
类型擦除
泛型是Java在编译时的语法糖,在运行时,泛型类型信息会被擦除。这是Java泛型与C#模板的核心区别。
什么是类型擦除?
编译器在编译时会:
- 将泛型代码替换为原始类型(Raw Type)
- 自动插入必要的强制类型转换
- 生成桥接方法以保持多态性
java
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); // 自动插入强制转换
类型擦除代码验证
java
MyArray<Integer> myArray = new MyArray<>();
MyArray<String> myArray2 = new MyArray<>();
System.out.println(myArray);
System.out.println(myArray2);
输出结果:
MyArray@1b6d3586
MyArray@4554617c
类型擦除带来的限制
1. 运行时无法区分泛型类型
java
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true,都是 ArrayList
2. 不能使用 instanceof 检查泛型类型
java
if (list instanceof List<String>) // 编译错误
3. 不能创建泛型数组
java
T[] array = new T[10]; // 编译错误
// 可以这样绕过:
List<T>[] array = (List<T>[]) new List[10]; // 但会有警告
4. 静态上下文中不能使用泛型类型参数
java
public class GenericClass<T> {
private static T instance; // 编译错误
public static T getInstance() { ... } // 编译错误
}
泛型在实际开发中的最佳实践
优先使用泛型,避免原始类型
java
// 不推荐:原始类型
List list = new ArrayList();
// 推荐:泛型
List<String> list = new ArrayList<>();
使用泛型方法替代通配符(当类型参数有关联时)
java
// 使用通配符
public void swap(List<?> list, int i, int j) {
// 无法直接实现,因为不能从 List<?> 中取出元素再放回
}
// 使用泛型方法
public <T> void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
复杂场景应用:构建类型安全的 Builder 模式
java
public class Person {
private String name;
private Integer age;
private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private Integer age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Person build() {
// 可以添加校验逻辑
if (name == null || age == null) {
throw new IllegalStateException("name and age are required");
}
return new Person(this);
}
}
}
常见陷阱与注意事项
不能使用基本类型作为类型参数
java
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // 使用包装类
泛型方法中的类型推断
java
// 泛型方法
public static <T> T getValue(T t) {
return t;
}
// 调用时可以显式指定类型
String s = GenericClass.<String>getValue("hello");
// 也可以让编译器推断
String s = getValue("hello");
可变参数与泛型
Java 在可变参数中使用泛型时会产生堆污染警告,可以使用 @SafeVarargs 注解抑制:
java
@SafeVarargs
public static <T> List<T> asList(T... elements) {
return Arrays.asList(elements);
}