Skip to content

泛型边界

边界是什么?

泛型默认是无界的——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 可以调用边界类型及其父类型的方法
  • 多边界:用 & 连接,但只能有一个类是边界

合理使用边界,既能保持泛型的灵活性,又能获得编译器的类型检查支持。

基于 VitePress 构建