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

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

大家好,今天我们来深入探讨Java泛型中的一个重要且稍微复杂的部分:通配符的上下界以及与之密切相关的PECS原则。理解这些概念对于编写健壮、灵活且类型安全的代码至关重要。

1. 泛型基础回顾

在深入通配符之前,我们先简单回顾一下泛型的基本概念。泛型允许我们在定义类、接口和方法时使用类型参数,从而实现代码的重用,并在编译时提供类型检查。

例如,一个简单的泛型类 Box<T>

class Box<T> {
    private T t;

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

    public T get() {
        return t;
    }
}

public class GenericExample {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.set(10);
        Integer value = integerBox.get(); // No cast needed!

        Box<String> stringBox = new Box<>();
        stringBox.set("Hello");
        String text = stringBox.get();
    }
}

在上面的例子中,T 是类型参数。我们可以用具体的类型(如 IntegerString)替换它,从而创建不同类型的 Box 对象。

2. 为什么需要通配符?

虽然泛型提供了类型安全和代码重用,但在某些情况下,直接使用类型参数会带来一些限制。考虑以下场景:

public class GenericProblem {
    public static void printList(List<Object> list) {
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        // printList(stringList); // Compilation error!
    }
}

这段代码尝试将 List<String> 传递给接受 List<Object>printList 方法。尽管 StringObject 的子类,但 List<String> 并不是 List<Object> 的子类型。这是因为泛型类型是不变的(invariant)。

不变性意味着,即使 AB 的子类, GenericType<A> 也不是 GenericType<B> 的子类。 为了解决这个问题,我们需要通配符。

3. 通配符: ?

通配符 ? 表示未知类型。它允许我们编写可以处理多种类型参数的泛型方法。

public class WildcardExample {
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        printList(stringList); // Works now!

        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);

        printList(integerList); // Also works!
    }
}

现在 printList 方法可以接受任何类型的 List,因为 List<?>List<String>List<Integer> 的父类型。 但是,使用 ? 也有一个限制:你不能向使用 ? 的集合中添加元素,因为编译器无法确定集合中元素的具体类型。

public class WildcardProblem {
    public static void addToList(List<?> list, Object element) {
        // list.add(element); // Compilation error!
    }
}

编译器会阻止向 List<?> 添加任何元素,除了 null。 这是因为编译器无法确定 list 中元素的具体类型,因此无法保证类型安全。 为了解决这个问题,我们需要通配符的上下界。

4. 上界通配符: ? extends T

上界通配符 ? extends T 表示未知类型,但它必须是 TT 的子类。 这允许我们读取集合中的元素,并将其视为 T 类型。

public class UpperBoundWildcard {
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0;
        for (Number n : list) {
            sum += n.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        System.out.println("Sum of integers: " + sumOfList(integerList));

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(1.5);
        doubleList.add(2.5);
        System.out.println("Sum of doubles: " + sumOfList(doubleList));
    }
}

在上面的例子中,sumOfList 方法接受一个 List,其元素类型是 NumberNumber 的子类。 这意味着我们可以将 List<Integer>List<Double> 等传递给该方法。

? 类似,你通常不能向使用 ? extends T 的集合中添加元素,因为编译器无法确定集合中元素的具体类型。 例如,如果你有一个 List<? extends Number>,编译器无法确定它是 List<Integer> 还是 List<Double>。 如果你尝试添加一个 Integer,但集合实际上是 List<Double>,则会导致类型错误。 例外情况是添加 null, 因为 null 可以赋值给任何引用类型。

5. 下界通配符: ? super T

下界通配符 ? super T 表示未知类型,但它必须是 TT 的父类。 这允许我们向集合中添加 TT 的子类型的元素。

public class LowerBoundWildcard {
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        addIntegers(integerList);
        System.out.println("Integer list: " + integerList);

        List<Number> numberList = new ArrayList<>();
        addIntegers(numberList);
        System.out.println("Number list: " + numberList);

        List<Object> objectList = new ArrayList<>();
        addIntegers(objectList);
        System.out.println("Object list: " + objectList);
    }
}

在上面的例子中,addIntegers 方法接受一个 List,其元素类型是 IntegerInteger 的父类。 这意味着我们可以将 List<Integer>List<Number>List<Object> 等传递给该方法。

但是,你不能保证从使用 ? super T 的集合中读取的元素的类型。 你只能保证读取的元素是 Object 类型。 这是因为集合可能包含 T 的任何父类的实例。 例如,如果你有一个 List<? super Integer>,它可能是 List<Number>List<Object>。 如果它是 List<Number>,则它可以包含 Double 类型的元素。

6. PECS原则:Producer Extends, Consumer Super

PECS原则是使用通配符上下界的一个重要指导原则。 PECS是 "Producer Extends, Consumer Super" 的缩写,它告诉我们什么时候使用 extends (上界通配符) 和什么时候使用 super (下界通配符)。

  • Producer Extends (生产者使用 extends): 如果你需要从集合中读取数据(即,集合是生产者),则使用 ? extends T。 例如,List<? extends Number> 允许你从列表中读取 Number 类型的元素。

  • Consumer Super (消费者使用 super): 如果你需要向集合中写入数据(即,集合是消费者),则使用 ? super T。 例如,List<? super Integer> 允许你向列表中添加 Integer 类型的元素。

如果既要读取数据,又要写入数据,则不要使用通配符。

PECS原则总结:

操作 集合角色 使用通配符 通配符类型 示例
读取数据 生产者 ? extends T List<? extends Number>
写入数据 消费者 ? super T List<? super Integer>
既读又写 List<Integer>

7. PECS原则的应用案例

7.1. 复制集合

考虑一个复制集合的场景。 我们需要从一个集合中读取元素,并将它们添加到另一个集合中。

public class PECSExample {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (T element : src) {
            dest.add(element);
        }
    }

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

        List<Number> numberList = new ArrayList<>();
        copy(numberList, integerList);
        System.out.println("Number list: " + numberList);

        List<Object> objectList = new ArrayList<>();
        copy(objectList, integerList);
        System.out.println("Object list: " + objectList);
    }
}

在这个例子中,src 集合是生产者,我们从中读取元素,因此我们使用 ? extends Tdest 集合是消费者,我们向其中写入元素,因此我们使用 ? super T

7.2. 比较器

再考虑一个使用比较器的场景。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Animal implements Comparable<Animal> {
    private int age;

    public Animal(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Animal other) {
        return Integer.compare(this.age, other.age);
    }
}

class Dog extends Animal {
    public Dog(int age) {
        super(age);
    }
}

public class ComparatorExample {
    public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
        Collections.sort(list, comparator);
    }

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

        // 使用 Animal 类型的 Comparator 对 Dog 列表进行排序
        Comparator<Animal> animalComparator = Comparator.comparingInt(Animal::getAge);
        sort(dogList, animalComparator);

        for (Dog dog : dogList) {
            System.out.println("Dog age: " + dog.getAge());
        }
    }
}

在这个例子中,Comparator 是一个消费者,它消费 T 类型的对象。 因此,我们使用 ? super T。 这允许我们使用比较 T 的父类的比较器来比较 T 类型的对象。

8. 类型擦除 (Type Erasure)

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

类型擦除会影响泛型的某些行为,例如:

  • 不能使用基本类型作为类型参数(例如,List<int> 是不允许的,必须使用 List<Integer>)。
  • 在运行时无法获取泛型类型信息。

9. 总结

通配符上下界是Java泛型中一个强大而复杂的特性。 通过使用 ? extends T? super T,我们可以编写更灵活、更通用的代码,同时保持类型安全。 PECS原则是使用通配符的一个重要指导原则,它告诉我们什么时候使用 extends,什么时候使用 super。 深入理解通配符的上下界和PECS原则,能写出更优雅更健壮的Java泛型代码。

发表回复

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