变性是OOP语言不变的大坑,Java的数组协变就是其中的一口老坑。因为最近踩到了,便做一个记录。顺便也提一下范型的变性。

解释数组协变之前,先明确三个相关的概念,协变、不变和逆变。

一、协变、不变、逆变

假设,我为一家餐馆写了这样一段代码

class Soup{public voidadd(T t) {}

}classVegetable { }class Carrot extends Vegetable { }

有一个范型类Soup,表示用食材T做的汤,它的方法add(T t)表示向汤中添加食材T。类Vegetable表示蔬菜,类Carrot表示胡萝卜。当然,Carrot是Vegetable的子类。

那么问题来了,Soup和Soup之间是什么关系呢?

第一反应,Soup应该是Soup的子类,因为胡萝卜汤显然是一种蔬菜汤。如果真是这样,那就看看下面的代码。其中Tomato表示西红柿,是Vegetable的另一个子类

Soup soup = new Soup();

soup.add(new Tomato());

第一句没问题,Soup是Soup的子类,所以可以将Soup的实例赋给变量soup。第二句也没问题,因为soup声明为Soup类型,它的add方法接收一个Vegetable类型的参数,而Tomato是Vegetable,类型正确。

但是,两句放在一起却有了问题。soup的实际类型是Soup,而我们给它的add方法传递了一个Tomato的实例!换言之,我们在用西红柿做胡萝卜汤,肯定做不出来。所以,把Soup视为Soup的子类在逻辑上虽然是通顺的,在使用过程中却是有缺陷的。

那么,Soup和Soup究竟应该是什么关系呢?不同的语言有不同的理解和实现。总结起来,有三种情况。

(1)如果Soup是Soup的子类,则称泛型Soup是协变的

(2)如果Soup和Soup是无关的两个类,则称泛型Soup是不变的

(3)如果Soup是Soup的父类,则称泛型Soup是逆变的。(不过逆变不常见)

理解了协变、不变和逆变的概念,再看Java的实现。Java的一般泛型是不变的,也就是说Soup和Soup是毫无关系的两个类,不能将一个类的实例赋值给另一个类的变量。所以,上面那段用西红柿做胡萝卜汤的代码,其实根本无法通过编译。

二、数组协变

Java中,数组是基本类型,不是泛型,不存在Array这样的东西。但它和泛型很像,都是用另一个类型构建的类型。所以,数组也是要考虑变性的。

与泛型的不变性不同,Java的数组是协变的。也就是说,Carrot[]是Vegetable[]的子类。而上一节中的例子已经表明,协变有时会引发问题。比如下面这段代码

Vegetable[] vegetables = new Carrot[10];

vegetables[0] = new Tomato(); // 运行期错误

因为数组是协变的,编译器允许把Carrot[10]赋值给Vegetable[]类型的变量,所以这段代码可以顺利通过编译。只有在运行期,JVM真的试图往一堆胡萝卜中插入一个西红柿的时候,才发现大事不好。所以,上面的代码在运行期会抛出一个java.lang.ArrayStoreException类型的异常。

数组协变性,是Java的著名历史包袱之一。使用数组时,千万要小心!

如果把例子中的数组替换为List,情况就不同了。就像这样

ArrayList vegetables = new ArrayList(); // 编译期错误

vegetables.add(new Tomato());

ArrayList是一个泛型类,它是不变的。所以,ArrayList和ArrayList之间并无继承关系,这段代码在编译期就会报错。

两段代码虽然都会报错,但通常情况下,编译期错误总比运行期错误好处理一些。

三、当泛型也想要协变、逆变

泛型是不变的,但某些场景里我们还是希望它能协变起来。比如,有一个天天喝蔬菜汤减肥的小姐姐

classGirl {public void drink(Soupsoup) {}

}

我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup和Soup。但受到不变性的限制,它们无法作为drink的参数。

要实现这一点,应该采用一种类似于协变性的写法

public void drink(Soup extends Vegetable> soup) {}

意思是,参数soup的类型是泛型类Soup,而T是Vegetable的子类(也包括Vegetable自己)。这时,小姐姐终于可以愉快地喝上胡萝卜汤和西红柿汤了。

但是,这种方法有一个限制。编译器只知道泛型参数是Vegetable的子类,却不知道它具体是什么。所以,所有非null的泛型类型参数均被视为不安全的。说起来很拗口,其实很简单。直接上代码

public void drink(Soup extends Vegetable>soup) {

soup.add(new Tomato()); //错误

soup.add(null); //正确

}

方法内的第一句会在编译期报错。因为编译器只知道add方法的参数是Vegetable的子类,却不知道它具体是Carrot、Tomato、或者其他的什么类型。这时,传递一个具体类型的实例一律被视为不安全的。即使soup真的是Soup类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。

但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。

同样,也有一种类似于逆变的方法

public void drink(Soup super Vegetable> soup) {}

这时,Soup中的T必须是Vegetable的父类。

这种情况就不存在上面的限制了,下面的代码毫无问题

public void drink(Soup super Vegetable>soup) {

soup.add(newTomato());

}

Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。

java数组的协变_Java数组协变与范型不变性相关推荐

  1. java 数组协变_java 数组协变

    标签: 在某些情况下,即使某个对象不是数组的基类型,我们也可以把它赋值给数组元素.这种属性叫做协变(covariance).在下面的情况下可以使用协变: 数组是引用类型数组. 在赋值的对象类型和数组基 ...

  2. java数组详解_java数组详解

    一维数组 1)   int[] a;   //声明,没有初始化 2)   int[] a=new int[5];   //初始化为默认值,int型为0 3)   int[] a={1,2,3,4,5} ...

  3. java获取数组的最小值_Java 数组获取最大和最小值的实例实现

    以下实例演示了如何通过 Collections 类的 Collections.max() 和 Collections.min() 方法来查找数组中的最大和最小值: Main.java 文件: impo ...

  4. js 数组 改变长度_Java数组,什么是Java数组?Java数组学习

    人的生命,似洪水奔流,不遇着岛屿和暗礁,难以激起美丽的浪花. 简介 数组是相同类型数据的有序集合 比如:[1,2,3] ['a','b','c'] 数组描述的是相同类型的若干个数据,按照一定的先后顺序 ...

  5. java数组初始化赋值_Java数组的三种初始化方式

    Java语言中数组必须先初始化,然后才可以使用.所谓初始化就是为数组的数组元素分配内存空间,并为每个数组元素附初始值. 注意:数组完成初始化后,内存空间中针对该数组的各个元素就有个一个默认值: 基本数 ...

  6. java数组末尾添加元素_java数组添加元素,java数组如何添加一个元素

    java数组如何添加元素 向数组里添加一个元素怎么添加,这儿总结有三种方法: 1.一般数组是不能添加元素的,因为他们在初始化时就已定好长度了,不能改变长度. 但有个可以改变大小的数组为ArrayLis ...

  7. java数组的声明_Java数组定义常用方法

    Java数组定义常用方法 Java中的数组.是一种简单的线性数据存储结构.他用牺牲自动扩展大小来换取与集合相比的唯一优势--查询效率的提升.Java中的数组有什么类型?我们要怎么定义这些数组呢?下面跟 ...

  8. java数组定义长度_JAVA数组的定义

    JAVA一维数组 一,注意 不可添加数组元素 不可改变数组长度 一个数组中的说有元素必须数据类型相同 二,创建方法三种 1直接添加元素 类型[] 数组名 = {元素,元素,元素,--}; int[] ...

  9. java数组变量定义_JAVA数组的定义及用法

    数组是有序数据的集合,数组中的每一个元素具有同样的数组名和下标来唯一地确定数组中的元素. 1. 一维数组 1.1 一维数组的定义 type arrayName[]; type[] arrayName; ...

最新文章

  1. JVM 与 Linux 的内存关系详解
  2. 计算概论(A)/基础编程练习1(8题)/4:求一元二次方程的根
  3. 【转】 ASP.NET 3.5中使用新的ListView控件
  4. blp模型 上读下写_CreditX在线借贷欺诈检测框架BLP
  5. 世上的人大都只会“飞鸽传书下载”,没人开发
  6. 华为P50 Pro最新渲染图曝光:后置造型有点奇怪
  7. 莫队(bzoj 2038: [2009国家集训队]小Z的袜子(hose))
  8. 读书总结--CRM与数据挖掘
  9. centos安装网易云音乐~
  10. html如何将设置文本效果,Word2013中通过设置文本效果格式来为文字添加特殊效果...
  11. 计算机专业学微机原理与接口技术,信息技术学院计算机科学与技术专业《微机原理与接口技术.doc...
  12. 如何更换IP?最简单的换IP方法
  13. TZOJ--5447: Irrational Division (博弈)
  14. Fly.js HTTP 请求解决方案
  15. 游戏攻略 Re:LieF ~親愛なるあなたへ~ (relief给挚爱的你)
  16. Excel导入SqlServer2012提示“消息7314”
  17. word怎么设置边距为80磅_Word排版不能忽视的「标尺」工具,6 种用法 80% 的人不知道!...
  18. 从“账房先生”到“中国巨型计算机之父”,慈云桂先后主导了中国四代计算机的研发...
  19. 510758-19-7,5-FAM-Alkyne高选择性和灵敏的荧光生物标记物,可用于标记碱性磷酸酶 (ALP)
  20. 高光谱学习---正交子空间投影法OSP(Orthogonal Subspace Projection)

热门文章

  1. java ado_Java进击C#——语法之ADO.NET
  2. numpy 数组 维度 大小 形状
  3. django两个服务器之间的通讯
  4. 朴素贝叶斯分类器和一般的贝叶斯分类器有什么区别
  5. 车道线检测--End-to-end Lane Detection through Differentiable Least-Squares Fitting
  6. 使用FFT来计算IFFT
  7. 【Dual-Path-RNN-Pytorch源码分析】loss函数:SI-SNR
  8. 报错解决:undefined reference to `snappy::MaxCompressedLength(unsigned long)'
  9. Git clone密码输入错误如何修改
  10. python增加一列数据计算年龄_无法使用python中的条件计算年龄。得到一个价值