当前位置: 主页 > JAVA语言

java泛型逆变与协变-java在泛型编程中的继承关系/C

发布时间:2023-06-12 07:09   浏览次数:次   作者:佚名

协变、逆变、抗变

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

第一个集合为:Animal、Dog , Dog extends Animal

第二个集合为:List 、List

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

协变(Covariance):List是List的子类型逆变(Contravariance): List是List的子类型抗变(Invariant): List与List二者间没有任何继承关系 数组的协变

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

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]);

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

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

泛型的抗变性

List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();
// objList = strList; 编译失败

c 泛型 协变 逆变_逆变 协变 java_java泛型逆变与协变

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

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

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

泛型类型的协变

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

协变的限制

List<Double> dList = new ArrayList<>();
List<? extends Number> nList = new ArrayList<Number>();
List<? extends Number> nList2 = new ArrayList<Double>();
// List nList2 = new ArrayList(); // 编译失败
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);

c 泛型 协变 逆变_逆变 协变 java_java泛型逆变与协变

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

但 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,使得含有泛型类型形参 与 返回值的方法发生了改变。

逆变 协变 java_c 泛型 协变 逆变_java泛型逆变与协变

含有泛型类型返回值的方法:可以正常获取含有泛型类型形参的方法:无法在传入任何非 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 nList2 = new ArrayList(); // 编译失败
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()); // 编译失败

启用逆变后,可以正常的在列表中添加元素,但可添加的元素类型为泛型类型自身及其子类型。

之所以能够添加泛型类型的子类类型元素java泛型逆变与协变java泛型逆变与协变,是因为下界限定符限定列表中的元素类型为泛型类型的父类类型,而泛型类型的子类也一定是泛型类型父类的子类。

逆变 协变 java_java泛型逆变与协变_c 泛型 协变 逆变

逆变的限制

无法在正常获取元素,因为不知道元素类型究竟是泛型类型的哪个父类型。这正是逆变的代价:无法在获取 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;
}

c 泛型 协变 逆变_逆变 协变 java_java泛型逆变与协变

协变与逆变的结合应用

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));
    }
}

协变、逆变小结

协变体现在方法的返回值类型为泛型类型时。

逆变体现在方法的参数类型为泛型类型时。

如下:

只读取,且类型满足协变关系,使用 ? extends T只写入,且类型满足逆变关系,使用 ? 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); // 编译失败
}