[JAVA冷知识]什么是逆变与协变?数组是否支持协变&逆变?泛型呢?

createh53个月前 (02-01)技术教程23

写在前面


  • 和小伙伴分享一些java小知识点,主要围绕下面几点:
  • 什么是逆变(contravariant)&协变(covariant)?
  • 数组支持协变&逆变吗?
  • 泛型支持协变&逆变吗?
  • 部分内容参考 《编写高质量代码(改善Java程序的151个建议)》
  • 博文理解有误的地方小伙伴留言私信一起讨论

生活不能等待别人来安排,要自己去争取和奋斗;而不论其结果是喜是悲,但可以慰藉的是,你总不枉在这世界上活了一场。有了这样的认识,你就会珍重生活,而不会玩世不恭;同时,也会给人自身注入一种强大的内在力量。 ——路遥《平凡的世界》


关于协变逆变到底是什么意思,其实很好理解,用一句话描述:(小伙伴们看到下面的话,会不会想到这不就是多态吗,哈,今天我们只看协变和逆变,关于多态的一些内容,如强制多态包含多态重载多态等之后有机会和小伙伴们分享)

协变 即指窄类型替换宽类型逆变宽类型覆盖窄类型

这里的窄类型子类(派生类),这里的宽类型父类(基类,超类),那这里的替换覆盖又是什么意思,这里就要说到OO(面相对象)六大设计原则之一的LSP(里氏代换原则 Liskov Substitution Principle),里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为

下面们结合代码就数组和泛型的协变和逆变进行分析

我们来看一段代码

package com.liruilong;

import java.util.Arrays;

/**
 * @Project_name: workspack
 * @Package: com.liruilong
 * @Description:
 * @Author: 1224965096@qq.com
 * @WeChat_Official_Accounts: 山河已无恙
 * @blog: https://liruilong.blog.csdn.net/
 * @Date: 2022/2/11  1:18
 */
public class CovariantDemo {

    public static void main(String[] args) {
        Number [] numbers = {1,1L,3d,2.0F};
        Arrays.stream(numbers).forEach(System.out::print);
    }
}

Number类是所有基本类型封装类的父类,同理基本类型封装类为Number类的子类,关于自动装箱和自动拆箱是java在JDK1.5的时候引入的新特性,我们这里不多讲,上面的代码可以正常编译,并且输出下面的内容,这里,数组里的基本类型装箱为封装类放到了堆中,这些封装类可以出现在Number类定义的数组中,说明子类可以替换了父类,即数组是满足协变的。

113.02.0
Process finished with exit code 0

既然数组支持协变,那么逆变呢?我们来看看

package com.liruilong;

import java.util.Arrays;

/**
 * @Project_name: workspack
 * @Package: com.liruilong
 * @Description:
 * @Author: 1224965096@qq.com
 * @WeChat_Official_Accounts: 山河已无恙
 * @blog: https://liruilong.blog.csdn.net/
 * @Date: 2022/2/11  1:18
 */
public class CovariantDemo {

    public static void main(String[] args) {
        Number [] numbers = {new Object()};
        Arrays.stream(numbers).forEach(System.out::print);
    }
}

这里我们把数组元素换成Object类,即所有类的父类,希望是可以通过父类来覆盖代替子类,但是直接编译报错,说明数组不支持直接逆变

Error:(17, 30) java: 不兼容的类型: java.lang.Object无法转换为java.lang.Number

数组不支持直接逆变,那么是否可以接见的实现逆变的,这里我么就要用到多态里的一种,强制多态,即强制类型转化试试

package com.liruilong;

import java.util.Arrays;

/**
 * @Project_name: workspack
 * @Package: com.liruilong
 * @Description:
 * @Author: 1224965096@qq.com
 * @WeChat_Official_Accounts: 山河已无恙
 * @blog: https://liruilong.blog.csdn.net/
 * @Date: 2022/2/11  1:18
 */
public class CovariantDemo {
    class A {

    }
    class B extends A{
      
    }

    public static void main(String[] args) {
        A a = new CovariantDemo().new A();
        B [] bs = {(B) a};
        Arrays.stream(bs).forEach(System.out::print);
    }
}

类型转化报错。说明对于数组的逆变来讲,是不支持逆变的,将父类强制转化为子类报类型转化异常,java并没有对这方面做限制。

Exception in thread "main" java.lang.ClassCastException: com.liruilong.CovariantDemo$A cannot be cast to com.liruilong.CovariantDemo$B
 at com.liruilong.CovariantDemo.main(CovariantDemo.java:24)

Process finished with exit code 1

通过上面代码,我们可以知道数组支持协变,不支持逆变,那泛型呢?对于协变和逆变是否支持

泛型不支持协变也不支持逆变,即不能把一个父类对象赋值给一个子类类型变量,相反也是同理。

下面我们看看代码

package com.liruilong;

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

/**
 * @Project_name: workspack
 * @Package: com.liruilong
 * @Description:
 * @Author: 1224965096@qq.com
 * @WeChat_Official_Accounts: 山河已无恙
 * @blog: https://liruilong.blog.csdn.net/
 * @Date: 2022/2/11  1:18
 */
public class CovariantDemo {
    public static void main(String[] args) {
        List ln = new ArrayList();
    }
}

java为了保证运行期安全性,必须保证泛型参数类型固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使为父子关系也不行。所以直接编译报错,即泛型不支持协变也不支持逆变.

Error:(17, 27) java: 不兼容的类型: java.util.ArrayList无法转换为java.util.List

但可以使用通配符(Wildcard)模拟协变逆变,通配符在编译期有效,在运行期必须为一个明确的类型

package com.liruilong;

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

/**
 * @Project_name: workspack
 * @Package: com.liruilong
 * @Description:
 * @Author: 1224965096@qq.com
 * @WeChat_Official_Accounts: 山河已无恙
 * @blog: https://liruilong.blog.csdn.net/
 * @Date: 2022/2/11  1:18
 */
public class CovariantDemo {
    public static void main(String[] args) {
        List< ? extends Number > list = new ArrayList();
    }
}

Number的子类型都可以为泛型类型参数,即允许NUmber所有的子类作为泛型参数类型,在运行期为一个具体的值.编译没有报错


Process finished with exit code 0

逆变同样也是可以,即泛型可以通过superextends来模拟实现协变和逆变,但是本身是不存在协变和逆变的,这里主要利用了泛型在编译器有效

List< ? super Integer> li = new ArrayList();

关于协变逆变就和小伙伴分享到这里,嗯,还有协变逆变方法,这里要简单说明下

协变方法: 即子类的方法返回值的类型比父类方法要窄,即该方法为协变方法,也称多态,覆写,重写

//子类的doStuff()方法返回值的类型比父类方法要窄,即该方法为协变方法,也称多态。
    class A{
        public  Number doStuff(){
            return 0;
        }
    }
    class B extends A{
        @Override
        public  Integer doStuff(){
            return 0;
        }
    }

逆变方法:子类的方法返回值的类型比父类方法宽,此时为逆变方法。虽然子类扩大了父类的输入返回参数,但是这里已经是重载了。

//子类的doSutff方法返回值的类型比父类方法宽,此时为逆变方法,
    class C {
        public Integer doStuff(Integer i) {
            return 0;
        }
    }
    class D extends C {
        public Number doStuff(Number i) {
            return 0;
        }
    }

相关文章

算法:有序数组的平方(Java版)(java 有序数组)

有序数组的平方题目描述:给定一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。示例:输入: nums = [-4,-1,0,3,10] 输出: [0,1...

Java数组详解(java数组操作方法)

数组,也叫Array,是由同一种数据类型按照一定的顺序排列的集合,给这个数组起一个名字。是一种数据类型。定义数组,在类型的后面加一个[]定义数组有两种方式①静态初始化 int[] num=new in...

趣味玩转数组:Java中的数组遍历技巧

当涉及到Java语言中的数组遍历和操作,我们可以从基本概念开始,逐步深入,以确保您理解得更全面。我们将覆盖以下主题:数组的基本概念声明和初始化数组数组的遍历常见的数组操作让我们一步一步来讲解这些内容:...

LeetCode-215-数组中的第K个最大元素

数组中的第K个最大元素题目描述:给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。示例说明...

刷题力扣349-两个数组的交集(两个数组的交集 ii)

这道题代码随想录用的是哈希数据结构,什么时候用哈希表,哈希表都是用来快速判断一个元素是否出现在集合里,相对于枚举的话,哈希表的时间复杂度只有O(1)。常见的三种哈希结构数组set(集合)map(映射)...

请讲解C#中数组的长度和范围与其他编程语言的区别与共性?

C# 中数组的长度和范围与其他编程语言的区别与共性数组是编程语言中常用的数据结构之一,各种语言对数组的长度和范围处理方式有所不同。以下是 C# 中数组的长度和范围的特性,以及与其他编程语言的对比。C#...