泛型边界
边界是什么?
泛型默认是无界的——T 可以是任何类型。但这带来一个问题:当你需要对 T 调用方法时,编译器不认识它。
比如:
java
public class Box<T> {
private T value;
public void print() {
System.out.println(value.doubleValue()); // 编译错误!T 不一定有 doubleValue()
}
}编译器不知道 T 是什么,所以不敢让你调用 doubleValue()。
解决方案:给泛型加上边界限制。
单边界
用 extends 限制 T 必须是某个类或接口的子类:
java
public class NumberBox<T extends Number> {
private T value;
public void printDouble() {
System.out.println(value.doubleValue()); // OK!编译器知道 T 是 Number
}
public int toInt() {
return value.intValue(); // 也能调用
}
}
// 使用
NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<Double> doubleBox = new NumberBox<>(); // OK
NumberBox<String> strBox = new NumberBox<>(); // 编译错误!String 不是 Number接口边界
边界不一定是类,也可以是接口:
java
// T 必须实现 Comparable 接口
public class Maximum<T extends Comparable<T>> {
public T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}多边界
一个泛型可以有多个边界,用 & 连接:
java
// T 必须是 Comparable 且 Serializable
public class Node<T extends Comparable<T> & Serializable> {
private T data;
public T getData() {
return data;
}
}多个边界中,只能有一个是类,其他必须是接口:
java
// 正确:类在前
<T extends Number & Comparable<T>>
// 错误:两个都是类
<T extends Number & Thread> // 编译错误!为什么?因为 Java 是单继承,多个类无法同时满足。
通配符边界
通配符也可以有边界:
上界通配符 ? extends
java
// ? extends Number 表示「Number 或其子类」
public double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue(); // OK
}
return total;
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sum(ints)); // OK
System.out.println(sum(doubles)); // OK下界通配符 ? super
java
// ? super Integer 表示「Integer 或其父类」
public void addNumbers(List<? super Integer> list) {
list.add(1); // OK
list.add(2);
list.add(3);
}
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK边界与类型擦除
边界会影响类型擦除后的桥接:
java
public class Container<T extends Number> {
private T value;
public void set(T value) {
this.value = value;
}
}编译后擦除为:
java
public class Container {
private Number value;
public void set(Number value) {
this.value = value;
}
}编译器还会生成桥接方法来保持多态:
java
// 编译器自动添加
public void set(Object value) {
this.set((Number) value); // 桥接方法
}边界与运行时
边界信息在运行时会被擦除:
java
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
System.out.println(intList.getClass() == numList.getClass()); // true,都是 ArrayList
System.out.println(intList instanceof List<Number>); // 编译错误!泛型信息已擦除边界选择建议
| 场景 | 边界选择 | 原因 |
|---|---|---|
| 需要调用特定方法 | 边界限定 | 限定后才能调用 |
| 只做读写 | 无界 T | 无界更灵活 |
| 读操作多 | ? extends T | 适合生产者 |
| 写操作多 | ? super T | 适合消费者 |
| 既读又写 | 泛型方法 <T> | 灵活但复杂 |
实际应用
边界在集合中的应用
java
// 限制只能放数字
public class NumberCache<T extends Number> {
private T[] values;
public double sum() {
double total = 0;
for (T v : values) {
total += v.doubleValue();
}
return total;
}
}边界在反射中的应用
java
// 使用 Class<T> 和边界
public <T extends Comparable<T>> void sort(List<T> list) {
// T 一定是可比较的
Collections.sort(list);
}边界在泛型工厂中的应用
java
public class GenericFactory<T extends Product> {
private Class<T> clazz;
public GenericFactory(Class<T> clazz) {
this.clazz = clazz;
}
public T create() throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
// 使用
GenericFactory<Widget> factory = new GenericFactory<>(Widget.class);
Widget widget = factory.create();总结
边界是泛型的「能力放大器」:
- 无界泛型:
T只能调用Object的方法 - 有界泛型:
T可以调用边界类型及其父类型的方法 - 多边界:用
&连接,但只能有一个类是边界
合理使用边界,既能保持泛型的灵活性,又能获得编译器的类型检查支持。
