Java 泛型的协变与逆变

协变、逆变、抗变

协变,逆变,抗变等概念是从数学中来的,在编程语言Java/Kotlin/C#中主要应用在泛型上。描述的是两个类型集合之间的继承关系。

第一个集合为:Animal、Dog , Dog extends Animal
第二个集合为:List<Animal> List<Dog>

Java/Kotlin/C#中,由于DogAnimal的子类型,那么List<Dog>也是List<Animal>的子类型吗?实则不然,两个列表是两个完全不同的类型。协变、逆变、抗变描述的就是两个类型集合间的关系。

  1. 协变(Covariance):List<Dog>List<Animal>的子类型
  2. 逆变(Contravariance): List<Animal>List<Dog>的子类型
  3. 抗变(Invariant): List<Animal>List<Dog>二者间没有任何继承关系

数组的协变

由于历史原因,数组是默认支持协变的。

Object[] objArr = new Object[2];
String[] strArr = new String[] {"A"};
objArr = strArr;
// objArr[0] = 1; // 编译通过,但运行时会抛出:ArrayStoreException
objArr[0] = "B"; // success

System.out.println(objArr[0]);

将一个子类型数组的引用,协变后成功赋值给了父类型的数组引用。就会引发通过 objArrstrArr 赋值了一个其他子类型的元素,比如 Integer 10。所以 java 语言在运行时会检查并抛出 ArrayStoreException。很明显这种只能在运行时才暴露问题的机制很不友好,这种代码不应该被成功编译,或者就不支持协变这种机制。

java 在泛型编程中的做法是,支持协变逆变这两种特性,并同时在编译期检查数据类型的正确性。当我们在开发工具中,如 idea 编写 Stream 代码时,细心的同学会发现每次调用一个功能函数如 map、collect 后,当前的泛型类型都在动态变化,就犹如编译时类型推导

泛型的抗变性

List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();

// objList = strList; 编译失败

泛型不同数组,默认是具有抗变性的。比如上面的例子,即使 String 是 Object 的子类,但也无法直接扩展为 List<Object>。如果可以这么做,就会发生数组的 ArrayStoreException 的情况:

objList = strList;
objList.add(100);
String str = objList.get(0);

就如开头时提到的:List<String>List<Object> 就是两种不同的类型(虽然 String 与 Object 存在继承关系),二者类型没有任何关系。这样虽然代码上安全了许多,但大大降低了代码的灵活性。如有些场景,为了代码的通用性,仍需要协变、逆变的这种特性打破泛型的抗变性,使得可以处理更多泛型类型的数据。不然处理一些含有泛型的数据时,是很难做到更好的兼容的,只能针对于每种泛型类型都编写一个方法。下面会逐步介绍泛型的协变与逆变。

泛型类型的协变

前面说到在泛型中默认具有抗变性,那么如何打破这种限制?上界限定符: ? extends T

协变的限制
List<Double> dList = new ArrayList<>();
List<? extends Number> nList = new ArrayList<Number>();
List<? extends Number> nList2 = new ArrayList<Double>();
// List<? extends Number> nList2 = new ArrayList<Object>(); // 编译失败
nList = dList;
nList2 = dList;

nList = dList 编译通过!那么是否还会存在类似数组的 ArrayStoreException 的情况吗?我们尝试在 nList 中分别添加 Double、Integer 元素。

nList.add(1.23D); // 编译失败
nList.add(123); // 编译失败

这正是协变的代价:无法在向 list 中添加没有 ? extends 修饰时(协变前)能正常添加的数据。只是泛型的做法更加直接,无论这个元素的类型是否为正确的,都不让添加,避免存储异常。假如可以新增数据,在 nList 添加了一个 Long 元素:

nList = dList;
nList.add(10L);

是不是又发生了存储异常的情况。

但 null 是一种特殊情况

nList.add(null); // 编译成功
协变的好处

可以正常的获取元素,元素类型为协变父类型:Number

Number number = nList.get(0);
协变生效的位置

生效位置:方法形参 & 返回值

为了直观的查看协变的机制,我们不在使用 ArrayList,而是通过一些自定义的类来进行测试。

static class AnimalFactory<T> {
    public T provide() {return null;}
    public void receive(T t) {}
}

static class Animal {}

static class Cat extends Animal {}

static class Dog extends Animal {}

public static void test() {
    // 协变前
    AnimalFactory<Animal> factory = new AnimalFactory<>();
    Animal provide1 = factory.provide();
    factory.receive(new Cat());

    // 协变后
    AnimalFactory<? extends Animal> factory2 = new AnimalFactory<>();
    Animal provide2 = factory2.provide(); // 返回值类型为泛型类型
    // factory2.receive(new Cat()); // 方法形参为泛型类型 (编译失败)
    factory2.receive(null); // 但可以传递 null
}

factory2 的泛型类型已经从 Animal 协变为了 ? extends Animal,使得含有泛型类型形参 与 返回值的方法发生了改变。

  1. 含有泛型类型返回值的方法:可以正常获取
  2. 含有泛型类型形参的方法:无法在传入任何非 null 的实例
协变的应用

编写一个方法,可以对泛型类型为 Number 的列表进行浮点数求和统计

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

    List<Double> dList = new ArrayList<>();
    dList.add(1D);
    dList.add(2D);

    double v1 = doubleSum(bList);
    double v2 = doubleSum(dList);

    System.out.println("v1:" + v1 + "\tv2:" + v2);
}

static double sum2Double(List<? extends Number> numbers) {
    double res = 0;
    for (Number number : numbers) {
        res += number.doubleValue();
    }
    return res;
}

泛型类型的逆变

逆变与协变是相对的,表示泛型类型可为指定类型自身及其父类。通过下界限定符: ? super T 进行声明。

逆变的好处
List<Object> objList = new ArrayList<>();
List<? super Number> nList = new ArrayList<Number>();
List<? super Number> nList2 = new ArrayList<Object>();
// List<? super Number> nList2 = new ArrayList<Double>(); // 编译失败
nList = objList;

nList = objList 编译通过!那么是否还会存在类似数组的 ArrayStoreException 的情况吗?我们尝试在 nList 中分别添加 Number 、Object 元素。

nList.add(new Number() {
    @Override
    public int intValue() {
        return 0;
    }

    @Override
    public long longValue() {
        return 0;
    }

    @Override
    public float floatValue() {
        return 0;
    }

    @Override
    public double doubleValue() {
        return 0;
    }
});
nList.add(1.23D);
// nList.add(new Object()); // 编译失败

启用逆变后,可以正常的在列表中添加元素,但可添加的元素类型为泛型类型自身及其子类型
之所以能够添加泛型类型的子类类型元素,是因为下界限定符限定列表中的元素类型为泛型类型的父类类型,而泛型类型的子类也一定是泛型类型父类的子类

逆变的限制

无法在正常获取元素,因为不知道元素类型究竟是泛型类型的哪个父类型。这正是逆变的代价:无法在获取 list 中添加没有 ? super 修饰时(协变前)能正常获取的数据

Number n = nList.get(0); // 编译失败

但 Object 是一种特殊情况,因为它是一切对象的父类。

Object obj = nList.get(0);
逆变生效的位置

生效位置:方法形参 & 返回值

static class AnimalFactory<T> {
    public T provide() {return null;}
    public void receive(T t) {}
}

static class Animal {}

static class Cat extends Animal {}

static class Dog extends Animal {}

public static void test() {
    // 逆变前
    AnimalFactory<Animal> factory1 = new AnimalFactory<>();
    Animal provide1 = factory1.provide();
    factory1.receive(new Cat());

    // 逆变后
    AnimalFactory<? super Animal> factory2 = new AnimalFactory<>();
    // Animal provide2 = factory2.provide(); // 返回值类型为泛型类型 (编译失败)
    // factory2.receive(new Object()); // 方法形参为泛型类型的父类 (编译失败)
    factory2.receive(new Animal()); // 方法形参为泛型类型自身
    factory2.receive(new Cat()); // 方法形参为泛型类型的子类
}
逆变的应用

编写一个方法,可以对泛型类型为 Number 的列表进行数据过滤

static <T> Collection<T> remove(Collection<T> col, Predicate<? super T> filter) {
     List<T> removeList = new ArrayList<>();
     for (T t : col) {
         if (filter.test(t)) {
             removeList.add(t);
         }
     }
     col.removeAll(removeList);
     return col;
}

协变与逆变的结合应用

public static void main(String[] args) {
    List<Integer> list1 = Lists.newArrayList(1, 2, 3);
    List<Integer> list2 = Lists.newArrayList(4, 5, 6);
    copy(list1, list2);
    System.out.println(list1); // [1, 2, 3]
    System.out.println(list2); // [1, 2, 3, 4, 5, 6]
}

public static <T> void copy(List<? extends T> src, List<? super T> dest) {
    int size = src.size();
    for (int i = 0; i < size; i++) {
        dest.add(i, src.get(i));
    }
}

协变、逆变小结

协变体现在方法的返回值类型为泛型类型时。
逆变体现在方法的参数类型为泛型类型时。
如下:

  1. 只读取,且类型满足协变关系,使用 ? extends T
  2. 只写入,且类型满足逆变关系,使用 ? super T

任意通配符

通配符 ?,表示任意类型。仅有协变、逆变的两种特殊情况。

public static void any(List<?> list) {
    Object o = list.get(0);
    list.add(0, null);
    list.add(0, new Object()); // 编译失败
}

extends 通配符的其他应用

应用一:另一种方法形参类型限定
static class Animal {}

static class Cat extends Animal {}

static class Dog extends Animal {}

// 限定泛型类型为 Animal 与其子类型,返回值类型只能为 Animal。语义同 createAnimal2
public static Animal createAnimal1(Class<? extends Animal> animalClass) {
    try {
        return animalClass.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

// 结合方法一、二的特性
// 限定泛型类型为 Animal 与其子类型,返回值类型只能为 Animal。语义同 createAnimal1
public static <T extends Animal> Animal createAnimal2(Class<T> animalClass) {
    try {
        return animalClass.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

// 限定泛型类型为 Animal 与其子类型,返回值类型可以为具体的传入的 T 类型,而不是只能返回 Animal 类型
public static <T extends Animal> T createAnimal3(Class<? extends T> animalClass) {
    try {
        return animalClass.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public static void extendsTest1() {
    Animal cat1 = createAnimal1(Cat.class);
    Animal dog1 = createAnimal1(Dog.class);
    Animal animal1 = createAnimal1(Animal.class);

    Animal cat2 = createAnimal2(Cat.class);
    Animal dog2 = createAnimal2(Dog.class);
    Animal animal2 = createAnimal2(Animal.class);

    Cat cat3 = createAnimal3(Cat.class);
    Dog dog3 = createAnimal3(Dog.class);
    Animal animal3 = createAnimal3(Animal.class);
}
应用二:应用在类、接口泛型
abstract static class Person {
    public void born() { System.out.println("嘤嘤嘤"); }
}

static class Man extends Person {}

static class Woman extends Person {}

static class PersonFactory<T extends Person> {
    public T create(Class<T> personClass) {
        try {
            T t = personClass.newInstance();
            t.born();
            return t;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

public static void extendsTest2() {
    PersonFactory<Man> manFactory = new PersonFactory<>();
    Man man = manFactory.create(Man.class);
    // manFactory.create(Woman.class); // 编译失败

    PersonFactory<Woman> womanFactory = new PersonFactory<>();
    Woman woman = womanFactory.create(Woman.class);
    // womanFactory.create(Man.class); // 编译失败
}
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值