Java的泛型:通配符(Wildcard)上下界与PECS原则的深度应用

Java 泛型:通配符(Wildcard)上下界与 PECS 原则的深度应用

各位朋友,大家好!今天我们来深入探讨 Java 泛型中的一个重要且略微复杂的部分:通配符(Wildcard)以及它与上下界结合使用,以及如何利用 PECS 原则来指导我们的泛型设计。掌握这些概念对于编写类型安全、灵活且可维护的 Java 代码至关重要。

1. 泛型基础回顾

在深入通配符之前,我们先快速回顾一下泛型的基本概念。泛型允许我们在定义类、接口和方法时使用类型参数,从而实现代码的类型安全和重用。例如:

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.set(10);
        Integer integerValue = integerBox.get();
        System.out.println(integerValue);

        Box<String> stringBox = new Box<>();
        stringBox.set("Hello");
        String stringValue = stringBox.get();
        System.out.println(stringValue);
    }
}

在这个例子中,Box<T> 是一个泛型类,T 是类型参数。我们可以用不同的类型替换 T,例如 IntegerString,从而创建不同类型的 Box 对象。

2. 为什么要使用通配符?

虽然泛型提供了类型安全,但在某些情况下,直接使用类型参数会限制代码的灵活性。考虑以下情况:

假设我们有一个打印 Box 中内容的函数:

public static void printBox(Box<Integer> box) {
    System.out.println(box.get());
}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>();
    integerBox.set(10);
    printBox(integerBox);

    //Box<Number> numberBox = new Box<>();
    //numberBox.set(10.5);
    //printBox(numberBox); // 编译错误!
}

这个函数只能接受 Box<Integer> 类型的参数。即使 IntegerNumber 的子类,我们也不能将 Box<Number> 传递给 printBox 函数。这是因为 Box<Integer>Box<Number> 是不同的类型,它们之间没有继承关系。

为了解决这个问题,我们需要使用通配符。

3. 通配符:?

通配符 ? 表示一个未知类型。它可以用来表示任何类型。例如,Box<?> 表示一个 Box,它的类型参数可以是任何类型。

我们可以修改 printBox 函数,使其接受 Box<?> 类型的参数:

public static void printBox(Box<?> box) {
    System.out.println(box.get());
}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>();
    integerBox.set(10);
    printBox(integerBox);

    Box<Number> numberBox = new Box<>();
    numberBox.set(10.5);
    printBox(numberBox); // 现在可以编译了!
}

现在,printBox 函数可以接受任何类型的 Box 对象。

通配符的限制:

虽然通配符增加了灵活性,但也带来了一些限制。在使用 Box<?> 时,我们无法向 Box 中添加元素,因为我们不知道它的具体类型。

public static void processBox(Box<?> box) {
    // box.set(10); // 编译错误!无法确定类型
    Object value = box.get(); // 可以读取
    System.out.println(value);
}

4. 上界通配符:? extends Type

上界通配符 ? extends Type 表示类型参数必须是 TypeType 的子类。例如,Box<? extends Number> 表示一个 Box,它的类型参数必须是 NumberNumber 的子类,比如 IntegerDouble 等。

public static void printNumberBox(Box<? extends Number> box) {
    Number number = box.get(); // 可以安全地读取 Number 类型
    System.out.println(number);
    // box.set(10); // 编译错误!无法确定具体类型
}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>();
    integerBox.set(10);
    printNumberBox(integerBox);

    Box<Double> doubleBox = new Box<>();
    doubleBox.set(10.5);
    printNumberBox(doubleBox);

    //Box<String> stringBox = new Box<>();
    //stringBox.set("Hello");
    //printNumberBox(stringBox); // 编译错误! String 不是 Number 的子类
}

使用上界通配符,我们可以安全地从 Box 中读取 Number 类型的值,但仍然无法向 Box 中添加元素,因为我们不知道它的具体类型。

5. 下界通配符:? super Type

下界通配符 ? super Type 表示类型参数必须是 TypeType 的父类。例如,Box<? super Integer> 表示一个 Box,它的类型参数必须是 IntegerInteger 的父类,比如 NumberObject 等。

public static void addIntegerToBox(Box<? super Integer> box) {
    box.set(10); // 可以安全地添加 Integer 类型
    //Integer integer = box.get(); // 编译错误!无法确定返回类型是 Integer
    Object object = box.get(); //只能返回Object类型
    System.out.println(object);

}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>();
    addIntegerToBox(integerBox); // 可以编译

    Box<Number> numberBox = new Box<>();
    addIntegerToBox(numberBox); // 可以编译

    Box<Object> objectBox = new Box<>();
    addIntegerToBox(objectBox); // 可以编译

    //Box<String> stringBox = new Box<>();
    //addIntegerToBox(stringBox); // 编译错误!String 不是 Integer 的父类
}

使用下界通配符,我们可以安全地向 Box 中添加 Integer 类型的值,但无法安全地从中读取 Integer 类型的值,因为我们不知道它的具体类型。只能获取到Object类型。

6. PECS 原则:Producer Extends, Consumer Super

PECS 原则是一个重要的泛型设计原则,它可以帮助我们决定何时使用上界通配符和下界通配符。PECS 是 Producer Extends, Consumer Super 的缩写,意思是:

  • Producer Extends (生产者使用 extends): 如果你需要从泛型类型中读取数据(例如,从 Collection 中读取元素),那么应该使用上界通配符 ? extends Type
  • Consumer Super (消费者使用 super): 如果你需要向泛型类型中写入数据(例如,向 Collection 中添加元素),那么应该使用下界通配符 ? super Type

表格总结:

操作 使用场景 通配符类型 示例
读取数据 从泛型类型中读取数据,例如从 List 中获取元素,或者从 Box 中获取值。 ? extends Type List<? extends Number> numbers = new ArrayList<Integer>(); Number n = numbers.get(0);
写入数据 向泛型类型中写入数据,例如向 List 中添加元素,或者向 Box 中设置值。 ? super Type List<? super Integer> numbers = new ArrayList<Number>(); numbers.add(10);
既读又写 如果你需要同时读取和写入数据,那么不应该使用通配符。使用确定的类型参数。 Type List<Integer> numbers = new ArrayList<>(); numbers.add(10); Integer n = numbers.get(0);

举例说明 PECS 原则:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class PECSExample {

    // 从 source 中读取元素,添加到 destination 中
    public static <T> void copy(List<? super T> destination, List<? extends T> source) {
        for (T element : source) {
            destination.add(element);
        }
    }

    //将集合中的数据都打印出来
    public static void printCollection(Collection<?> col) {
        for (Object o : col) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);

        List<Number> numberList = new ArrayList<>();
        numberList.add(3.14);
        numberList.add(2.71);

        // 将 integerList 中的元素复制到 numberList 中
        copy(numberList, integerList); // numberList 作为 Consumer,使用 ? super Integer,integerList 作为 Producer, 使用 ? extends Integer

        printCollection(numberList);
    }
}

在这个例子中,copy 方法将 source 中的元素复制到 destination 中。source 是一个生产者,因为它提供数据;destination 是一个消费者,因为它接收数据。因此,我们使用 ? extends T 来限制 source 的类型,使用 ? super T 来限制 destination 的类型。

不使用 PECS 原则可能导致的问题:

如果我们没有遵循 PECS 原则,可能会导致编译错误或运行时异常。例如,如果我们尝试将 List<Integer> 传递给一个需要 List<Number> 的方法,就会导致编译错误。

7. 泛型擦除与通配符

需要注意的是,Java 中的泛型是通过类型擦除实现的。这意味着在编译时,泛型类型信息会被擦除,替换为原始类型。例如,List<Integer>List<String> 在运行时都是 List 类型。

通配符也会受到类型擦除的影响。在使用通配符时,编译器会尽力推断类型,但有时无法确定具体类型,从而导致一些限制。这就是为什么我们无法向 Box<?>Box<? extends Number> 中添加元素,因为编译器无法确定它们的具体类型。

8. 通配符的嵌套使用

通配符可以嵌套使用,以表达更复杂的类型关系。例如:

public class NestedExample {

    public static void processList(List<? extends List<? extends Number>> listOfLists) {
        // 可以安全地读取 Number 类型的值
        for (List<? extends Number> list : listOfLists) {
            for (Number number : list) {
                System.out.println(number);
            }
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> listOfIntegerLists = new ArrayList<>();
        listOfIntegerLists.add(List.of(1, 2, 3));

        List<List<Double>> listOfDoubleLists = new ArrayList<>();
        listOfDoubleLists.add(List.of(1.1, 2.2, 3.3));

        List<List<Number>> listOfNumberLists = new ArrayList<>();
        listOfNumberLists.add(List.of(1, 2.2, 3));

        processList(listOfIntegerLists);
        processList(listOfDoubleLists);
        processList(listOfNumberLists);
    }
}

在这个例子中,List<? extends List<? extends Number>> 表示一个 List,它的元素是 List,而这些 List 的元素是 NumberNumber 的子类。

9. 总结与泛型设计的指导

Java 泛型中的通配符是一个强大的工具,可以提高代码的灵活性和可重用性。理解上界通配符、下界通配符以及 PECS 原则对于编写类型安全且可维护的泛型代码至关重要。在设计泛型 API 时,要根据数据流的方向(生产者还是消费者)来选择合适的通配符,从而最大程度地提高代码的灵活性和安全性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注