Java数组协变与范型不变性
变性是OOP语言不变的大坑,Java的数组协变就是其中的一口老坑。因为最近踩到了,便做一个记录。顺便也提一下范型的变性。
解释数组协变之前,先明确三个相关的概念,协变、不变和逆变。
一、协变、不变、逆变
假设,我为一家餐馆写了这样一段代码
class Soup<T> {
public void add(T t) {}
} class Vegetable { } class Carrot extends Vegetable { }
有一个范型类Soup<T>,表示用食材T做的汤,它的方法add(T t)表示向汤中添加食材T。类Vegetable表示蔬菜,类Carrot表示胡萝卜。当然,Carrot是Vegetable的子类。
那么问题来了,Soup<Vegetable>和Soup<Carrot>之间是什么关系呢?
第一反应,Soup<Carrot>应该是Soup<Vegetable>的子类,因为胡萝卜汤显然是一种蔬菜汤。如果真是这样,那就看看下面的代码。其中Tomato表示西红柿,是Vegetable的另一个子类
Soup<Vegetable> soup = new Soup<Carrot>();
soup.add(new Tomato());
第一句没问题,Soup<Carrot>是Soup<Vegetable>的子类,所以可以将Soup<Carrot>的实例赋给变量soup。第二句也没问题,因为soup声明为Soup<Vegetable>类型,它的add方法接收一个Vegetable类型的参数,而Tomato是Vegetable,类型正确。
但是,两句放在一起却有了问题。soup的实际类型是Soup<Carrot>,而我们给它的add方法传递了一个Tomato的实例!换言之,我们在用西红柿做胡萝卜汤,肯定做不出来。所以,把Soup<Carrot>视为Soup<Vegetable>的子类在逻辑上虽然是通顺的,在使用过程中却是有缺陷的。
那么,Soup<Carrot>和Soup<Vegetable>究竟应该是什么关系呢?不同的语言有不同的理解和实现。总结起来,有三种情况。
(1)如果Soup<Carrot>是Soup<Vegetable>的子类,则称泛型Soup<T>是协变的
(2)如果Soup<Carrot>和Soup<Vegetable>是无关的两个类,则称泛型Soup<T>是不变的
(3)如果Soup<Carrot>是Soup<Vegetable>的父类,则称泛型Soup<T>是逆变的。(不过逆变不常见)
理解了协变、不变和逆变的概念,再看Java的实现。Java的一般泛型是不变的,也就是说Soup<Vegetable>和Soup<Carrot>是毫无关系的两个类,不能将一个类的实例赋值给另一个类的变量。所以,上面那段用西红柿做胡萝卜汤的代码,其实根本无法通过编译。
二、数组协变
Java中,数组是基本类型,不是泛型,不存在Array<T>这样的东西。但它和泛型很像,都是用另一个类型构建的类型。所以,数组也是要考虑变性的。
与泛型的不变性不同,Java的数组是协变的。也就是说,Carrot[]是Vegetable[]的子类。而上一节中的例子已经表明,协变有时会引发问题。比如下面这段代码
Vegetable[] vegetables = new Carrot[10];
vegetables[0] = new Tomato(); // 运行期错误
因为数组是协变的,编译器允许把Carrot[10]赋值给Vegetable[]类型的变量,所以这段代码可以顺利通过编译。只有在运行期,JVM真的试图往一堆胡萝卜中插入一个西红柿的时候,才发现大事不好。所以,上面的代码在运行期会抛出一个java.lang.ArrayStoreException类型的异常。
数组协变性,是Java的著名历史包袱之一。使用数组时,千万要小心!
如果把例子中的数组替换为List,情况就不同了。就像这样
ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); // 编译期错误
vegetables.add(new Tomato());
ArrayList是一个泛型类,它是不变的。所以,ArrayList<Carrot>和ArrayList<Vegetable>之间并无继承关系,这段代码在编译期就会报错。
两段代码虽然都会报错,但通常情况下,编译期错误总比运行期错误好处理一些。
三、当泛型也想要协变、逆变
泛型是不变的,但某些场景里我们还是希望它能协变起来。比如,有一个天天喝蔬菜汤减肥的小姐姐
class Girl {
public void drink(Soup<Vegetable> soup) {}
}
我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup<Carrot>和Soup<Tomato>。但受到不变性的限制,它们无法作为drink的参数。
要实现这一点,应该采用一种类似于协变性的写法
public void drink(Soup<? extends Vegetable> soup) {}
意思是,参数soup的类型是泛型类Soup<T>,而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<Tomato>类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。
但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。
同样,也有一种类似于逆变的方法
public void drink(Soup<? super Vegetable> soup) {}
这时,Soup<T>中的T必须是Vegetable的父类。
这种情况就不存在上面的限制了,下面的代码毫无问题
public void drink(Soup<? super Vegetable> soup) {
soup.add(new Tomato());
}
Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。
Java数组协变与范型不变性的更多相关文章
- 为什么Java不允许创建范型数组
问题示例 List<Integer>[] intListArr = new ArrayList<Integer>[8]; // 编译时报错 能看到这么看似没啥问题的一个简单语句 ...
- Java数据结构与算法分析-第一章(引论)-Java中的范型<T,E>构件
一.为什么需要使用范型? 官方的说法是:Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 泛型的本质 ...
- Java范型随笔
最近在帝都好无聊啊, 排遣寂寞就只有让自己不要停下来,不断的思考了 QWQ; 最近做ndk, java有点忘了,突然看到了一些java范型方面的问题, 踌躇了一会, 想着想着,决定还是写个随笔记录下来 ...
- Java Comparator的范型类型推导问题
问题 在项目中,有一处地方需要对日期区间进行排序 我需要以日期区间的开始日为第一优先级,结束日为第二优先级进行排序 代码 我当时写的代码如下: List<Pair<LocalDate, L ...
- Java范型
泛型不用考虑对象的具体类型.优点在于,因为不用考虑对象的具体类型所以可以对一类对象执行一定的相同操作:缺点在于,因为没有考虑对象的具体类型所以就不能使用对象自带的接口函数.泛型的最佳用同是实现容器类. ...
- Java范型学习笔记
对于范型的使用或者说印象只有集合,其他地方即使使用过也不知道,反正就是只停留在List<E> Map<K, V>,最近刚好闲来无事,就找找资料学习一下:下列为个人学习总结,欢迎 ...
- java范型集合中的成员排序
范型集合中的类是JsonObject,不是自定义类,如果是自定义类就直接取要比较的字段值. ArrayList<JSONObject> TList = new ArrayList<J ...
- java数组与字符串相互转换、整型与字符串相互转换【详解】
java 数组->字符串 1.char数组(字符数组)->字符串 可以通过:使用String.copyValueOf(charArray)函数实现. 举例: char[] arr={ ...
- 关于java范型
1 范型只在编译阶段有效 编译器在编译阶段检查范型结果之后,就会将范型信息删除.范型信息不会进入运行时阶段. 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型. 2 不能对确定的范型 ...
随机推荐
- BZOJ_4443_[Scoi2015]小凸玩矩阵_二分+二分图匹配
BZOJ_4443_[Scoi2015]小凸玩矩阵_二分+二分图匹配 Description 小凸和小方是好朋友,小方给小凸一个N*M(N<=M)的矩阵A,要求小秃从其中选出N个数,其中任意两个 ...
- 毕业样本=[威尔士大学毕业证书]UWIC原件一模一样证书
威尔士大学毕业证[微/Q:2544033233◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&am ...
- (3)STM32使用HAL库操作外部中断——实战操作
有了上一篇的基础入门知识,使用Cube创建一个简单的外部中断就容易多了. 一.Cube配置 需求:使用PD10作为外部中断(下降沿触发)控制LED(PD12-PD14) 1.选型 STM32-F4-D ...
- WebGL学习之纹理贴图
为了使图形能获得接近于真实物体的材质效果,一般会使用贴图,贴图类型主要包括两种:漫反射贴图和镜面高光贴图.其中漫反射贴图可以同时实现漫反射光和环境光的效果. 实际效果请看demo:纹理贴图 2D纹理 ...
- 贝叶斯深度学习(bayesian deep learning)
本文简单介绍什么是贝叶斯深度学习(bayesian deep learning),贝叶斯深度学习如何用来预测,贝叶斯深度学习和深度学习有什么区别.对于贝叶斯深度学习如何训练,本文只能大致给个介绍. ...
- tcp套接字粘包解决办法
粘包只会出现在tcp,udp传输不会产生粘包现象.解决粘包的原理就是服务器预先向客户端发送客户端即将获取文件的大小. 第一版解决方案: 服务器: # Author : Kelvin # Date : ...
- asp.net core系列 54 IS4用客户端凭据保护API
一. 概述 本篇开始进入IS4实战学习,从第一个示例开始,该示例是 “使用客户端凭据保护API”,这是使用IdentityServer保护api的最基本场景.该示例涉及到三个项目包括:Identity ...
- WebGL three.js学习笔记 自定义顶点建立几何体
自定义顶点建立几何体与克隆 Three.js本身已经有很多的网格模型,基本已经够我们的使用,但是如果我们还是想自己根据顶点坐标来建立几何模型的话,Three.js也是可以的. 基本效果如图: 点击查看 ...
- CSharpGL(46)用Billboard绘制头顶文字
CSharpGL(46)用Billboard绘制头顶文字 本文介绍CSharpGL用Billboard绘制头顶文字的方法.效果如下图所示. 下载 CSharpGL已在GitHub开源,欢迎对OpenG ...
- 痞子衡嵌入式:超级好用的可视化PyQt GUI构建工具(Qt Designer)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是PyQt GUI构建工具Qt Designer. 痞子衡开博客至今已有好几年,一直以嵌入式开发相关主题的文章为主线,偶尔穿插一些其他技术 ...