Skip to content

集合泛型

读看上界,写看下界

类型构造器

image-20260331090315776

f表示类型构造器。一个类型被类型构造器处理后就是一个崭新的类型。

  • 协变:f(Dog)f(Animal)的子类

  • 逆变:f(Animal)f(Dog)的子类

  • 不变:f(Aniaml)f(Dog)没有任何关系

类型构造器可以是

  • 集合
java
List<Animal>与List<Dog>
  • 数组
java
Animal [] 与 Dog[]
  • 方法
java
say(Aniaml)与say(Dog)

数组是协变的

在Java中数组是允许协变的,这是因为Java1时还没有泛型。如果没有协变,我们需要对每一种类型的数组编写处理函数:

java
void handleDog(Dog [] list){}

void handleCat(Cat [] list){}

void handleTom(Tom [] list){}

开发者希望能够对数组进行通用处理。因此就设计了数组是允许协变的。

java
//三种类型的数组可以集成于一个数组中进行处理
void handleAnimal(Animal [] list){}

但是这种设计是存在缺陷的:

java
handleAnimal(new Dog[10]);
java
void handleAnimal(Animal [] list){
  list[0]=new Dog();
  list[1]=new Cat();//编译允许,但运行时会报错ArrayStoreException
}

允许数组协变是Java的设计缺陷

如果你想保证类型安全,避免运行时异常,可以使用 泛型 + List 代替数组

泛型默认是不变的

泛型模式是不变性的:

java
List<Animal> animals=new ArrayList<Dog>();//协变,编译错误
List<Dog> dogs=new ArrayList<Animal>();//逆变,编译错误

原因:

java
List<Animal> animals=new ArrayList<Dog>();//如果允许协变
animals.add(new Dog());
animals.add(new Cat());//类型不安全

让泛型协变

为什么要让泛型支持协变

希望可以像数组一样:

java
void handleDog(List<Dog> list){}

void handleCat(List<Cat> list){}

void handleTom(List<Tom> list){}
java
void handleAnimal(List<Animal> list){}

如何支持协变

在类型参数前添加? extends即可让泛型支持协变

java
void handleAnimal(List<? extends Aniaml> list)

此时以下调用都是合法的:

java
handleAnimal(new ArrayList<Dog> list);
handleAnimal(new ArrayList<Cat> list);
handleAnimal(new ArrayList<Tom> list);

根据数组的设计可以知道协变存在类型安全问题,因此泛型协变是只读的

java
void handleAnimal(List<? extends Animal> list){
  list.add(null);
  Animal a=list.get(0);//只读
  list.add(new Dog());//写入,编译错误
  list.add(new Cat());//写入,编译错误
}

Collection的设计原则

java
public interface Collection<T>{
  boolean add(E e);
  remove(Object o);
}
  • add 设置成只支持类型E添加是确保类型安全

  • remove设置成Object是确保方法更具通用性

java
List<String> list = new ArrayList<>();
Object obj = "a";
list.remove(obj); //也应该允许,因为Object也有equals方法。

让泛型逆变

在类型参数前添加? super即可让泛型支持逆变

java
void handleDog(List<? super Dog> list){}

此时以下调用是合法的:

java
handleDog(new ArrayList<Dog> list);
handleDog(new ArrayList<Animal> list);
java
void handleDog(List<? super Dog> list){
  Object obj=list.get(0);//无法读取
  list.add(new Dog());//写入允许,多态
  list.add(new Tom());//写入允许,多态
  list.add(new Animal());//写入,编译错误
}

PECS

这里的生产者和消费者指的是集合本身

Producer Extends (生产者使用 extends): 如果你只需要从集合中读取数据(集合是数据的生产者)。

Consumer Super (消费者使用 super): 如果你只需要往集合中写入数据(集合是数据的消费者)。

image-20260330105152816

类型写法能读能写
无界<?>Object
上界<? extends T>T
下界<? super T>Object
java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) { // src是生产者,从中读取T(或子类)对象
        dest.add(item);   // dest是消费者,向其中写入T(或父类)对象
    }
}

理解

  • List<Dog>

根据多态,List允许添加Dog,Teddy

  • List<? extends Dog>

通配符的作用是修饰整个复合类型,表示这个复合类型可以协变

协变是只读的,集合泛型是Dog

java
Dog dog =list.get(0)
  • List<? super Dog>

通配符的作用是修饰整个复合类型,表示这个复合类型可以逆变

逆变是只写的,集合泛型为Dog,根据多态:

java
list.add(new Dog());//写入允许,多态
list.add(new Tom());//写入允许,多态