限定通配符和非限定通配符_为什么我不信任通配符以及为什么我们仍然需要通配符...
限定通配符和非限定通配符
在将子类型多态性(面向对象)与参数多态性(泛型)相结合的任何编程语言中,都会出现方差问题。 假设我有一个字符串列表,键入List<String>
。 我可以将其传递给接受List<Object>
的函数吗? 让我们从这个定义开始:
interface List<T> {void add(T element);Iterator<T> iterator();...
}
破碎的协方差
凭直觉,我们可能首先认为应该允许这样做。 看起来不错:
void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>());
确实,包括Eiffel和Dart在内的某些语言确实接受此代码。 可悲的是,它是不完善的,如以下示例所示:
//Eiffel/Dart-like language with
//broken covariance:
void put(List<Object> list) {list.add(10);
}
put(ArrayList<String>());
在这里,我们将List<String>
传递给接受List<Object>
的函数,该函数尝试将Integer
添加到列表中。
Java对数组也会犯同样的错误。 以下代码编译:
//Java:
void put(Object[] list) {list[0]=10;
}
put(new String[1]);
它在运行时失败,并带有ArrayStoreException
。
使用地点差异
但是,对于通用类和接口类型,Java采用了不同的方法。 默认情况下,类或接口类型为invariant ,即:
- 当且仅当
U
与V
类型完全相同时,才可将L<V>
分配给L<V>
。
由于这在很多时候非常不方便,因此Java支持一种称为use-sitevariance的方法 ,其中:
L<U>
可分配给L<? extends V>
如果U
是V
的子类型,则L<? extends V>
,并且L<U>
可分配给L<? super V>
L<? super V>
如果U
是的超类型V
。
丑陋的语法? extends V
? extends V
或? super V
? super V
称为通配符 。 我们还说:
L<? extends V>
L<? extends V>
在V
是协变的,并且L<? super V>
L<? super V>
在V
是反变的。
由于Java的通配符表示法非常丑陋,因此在本讨论中我们将不再使用它。 取而代之的是,我们将分别使用关键字in
和out
来表示通变量和协方差。 从而:
L<out V>
在V
是协变的,并且L<in V>
是在逆变V
。
给定的V
称为通配符的边界 :
out V
是一个上限通配符,V
是其上限,并且in V
是下界通配符,V
是其下界。
从理论上讲,我们可以有一个具有上限和下限的通配符,例如L<out X in Y>
。
我们可以使用交集类型表示多个上限或多个下限,例如L<out U&V>
或L<in U&V>
。
请注意,类型表达式L<out Anything>
和L<in Nothing>
指的是完全相同的类型,并且此类型是L
的所有实例的超类型。 您会经常看到人们将通配符类型称为存在性类型 。 他们的意思是,如果我知道list
的类型为List<out Object>
:
List<out Object> list;
然后我知道存在一个未知的类型T
,这是Object
的子类型,因此list
的类型为List<T>
。
或者,我们可以从更宽泛的角度出发,说List<out Object>
是所有List<T>
类型的并集,其中T
是Object
的子类型。
在具有使用地点差异的系统中,以下代码无法编译:
void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>()); //error: List<String> not a List<Object>
但是这段代码可以做到:
void iterate(List<out Object> list) {Iterator<out Object> it = list.iterator();...
}
iterate(ArrayList<String>());
正确地,此代码无法编译:
void put(List<out Object> list) {list.add(10); //error: Integer is not a Nothing
}
put(ArrayList<String>());
现在我们在兔子洞的入口。 为了将通配符类型集成到类型系统中,同时像上面的示例一样拒绝不正确的代码,我们需要一种更为复杂的算法来替换类型实参。
会员输入使用地点差异
也就是说,当我们有一个泛型类型类似List<T>
有一种方法void add(T element)
,而不是仅仅直截了当代Object
的T
,就像我们做普通不变的类型,我们需要考虑的方差类型参数出现的位置。 在这种情况下, T
出现在List
类型的反位置 ,即作为方法参数的类型。 我不会在这里写下的复杂算法告诉我们,在此位置我们应该用Nothing
(底部类型)代替。
现在想象一下我们的List
接口有一个带有以下签名的partition()
方法:
interface List<T> {List<List<T>> partition(Integer length);...
}
List<out Y>
的partition()
的返回类型是什么? 好吧,在不损失精度的情况下,它是:
List<in List<in Y out Nothing> out List<in Nothing out Y>>
哎哟。
由于没有人在他们的头脑中想去考虑这样的类型,因此明智的语言会抛弃其中的一些界限,而留下这样的东西:
List<out List<out Y>>
这是可以接受的。 不幸的是,即使在这种非常简单的情况下,我们也已经远远超出了程序员可以轻松跟随类型检查器所做的工作的地步。
因此,这就是我不信任使用地点差异的原因所在:
- Ceylon设计的一个重要原则是,程序员应始终能够重现编译器的推理。 这是原因的一些与使用现场方差出现的复杂类型的非常困难。
- 它具有病毒性作用:一旦这些通配符类型在代码中立足,它们便开始传播,很难回到我的普通不变式类型。
申报地点差异
使用场所方差的一种更合理的选择是声明场所方差 ,在声明时我们指定泛型类型的方差。 这是我们在锡兰使用的系统。 在此系统下,我们需要将List
分为三个接口:
interface List<out T> {Iterator<T> iterator();List<List<T>> partition(Integer length);...
}interface ListMutator<in T> {void add(T element);
}interface MutableList<T>satisfies List<T>&ListMutator<T> {}
List
声明为协变类型, ListMutator
声明为协变类型, ListMutator
声明为MutableList
的不变子类型。
似乎对多个接口的需求似乎是声明站点差异的一个很大的缺点,但事实证明,将突变与读取操作分开是很有用的,并且:
- 变异运算通常是不变的,而
- 读取操作通常是协变的。
现在我们可以这样编写函数:
void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>());void put(ListMutator<Integer> list) {list.add(10);
}
put(ArrayList<String>()); //error: List<String> is not a ListMutator<Integer>
您可以在此处阅读有关声明位置差异的更多信息。
为什么我们在锡兰需要使用场所差异
可悲的是,Java没有声明站点差异,并且与Java的良好互操作对我们来说非常重要。 我不喜欢纯粹为了与Java互操作而在语言的类型系统中添加主要功能,因此多年来,我一直拒绝向Ceylon添加通配符。 最后,现实和实用性获胜,而我的顽固失去了。 因此,Ceylon 1.1现在具有带有单界通配符的使用站点差异。
我试图尽可能严格地限制此功能,仅提供体面的Java互操作所需的最低限度的功能。 这意味着,就像在Java中一样:
- 没有形式为
List<in X out Y>
双界通配符,并且 - 在类或接口定义的
extends
或satisfies
子句中不能出现通配符类型。
此外,与Java不同:
- 没有隐式界通配符,上限必须始终以显式形式编写,并且
- 不支持通配符捕获 。
通配符捕获是Java的一个非常聪明的功能,它利用了通配符类型的“现有”解释。 给定这样的通用函数:
List<T> unmodifiableList<T>(List<T> list) => ... :
Java让我调用unmodifiableList()
,传递一个通配符类型,如List<out Object>
,返回另一个通配符List<out Object>
,原因是存在一些未知的X
,这是Object
的子类型,对其进行调用是正确的。 也就是说,即使无法为任何T
将List<out Object>
类型分配给List<T>
,也认为该代码是正确的:
List<out Object> objects = .... ;
List<out Object> unmodifiable = unmodifiableList(objects);
在Java中,涉及通配符捕获的键入错误几乎是无法理解的,因为它们涉及未知且难以理解的类型。 我没有计划向锡兰添加对通配符捕获的支持。
试试看
使用站点差异已经实现,并且已经在Ceylon 1.1中起作用,如果您非常有动力,可以从GitHub获得。
即使此功能的主要动机是出色的Java互操作性,但在其他情况(可能很少见)中,通配符将很有用。 但是,这并不表示我们的方法有任何重大变化。 除极端情况外,我们将继续在Ceylon SDK中使用声明站点差异。
更新: 我只是意识到我忘了感谢Ross Tate的帮助,帮助我更好地了解了成员打字算法中使用地点差异的问题。 罗斯知道这些非常棘手的东西!
翻译自: https://www.javacodegeeks.com/2014/08/why-i-distrust-wildcards-and-why-we-need-them-anyway.html
限定通配符和非限定通配符
限定通配符和非限定通配符_为什么我不信任通配符以及为什么我们仍然需要通配符...相关推荐
- Java笔记:泛型、限定通配符与非限定通配符
目录 1 泛型 2 限定通配符与非限定通配符 2.1 限定通配符 2.2 非限定通配符 3 PECS(Producer Extends Consumer Super)原则 3.1 Producer E ...
- 什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制. 有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界.泛型类型必须用限定内的类型来进行初始化,否则会 ...
- Java中限定类名和非限定类名的区别
限定类名,就是类名全称,带包路径的用点隔开,例如: java.lang.String. 非限定(non-qualified)类名也叫短名,就是我们平时说的类名,不带包的,例如:String. 非限定类 ...
- (Java)全限定类名和非限定类名的区别
全限定类名:就是类名全称,带包路径的用点隔开,例如: java.lang.String. 即全限定名 = 包名+类型,又如: 这里的 T 就是类名,即非限定类名,mybatis.T 就是全限定类名 非 ...
- Java 全限定类名和非限定类名有何区别
为了方便说明,这里创建一个普通Java类来做演示 package com.csdn.test;public class HelloWorld {} 1.1 对于该类来说:全限定类名就是包名.类名,即c ...
- java中的全限定类名和非限定类名是什么意思?
全限定类名是指带包名的类名:(如java.lang.String) 非限定类名是不带包名的类名.(如String)
- java限定符_Java-泛型限定符、通配符
关键字:泛型.限定符.通配符 https://www.jianshu.com/p/897fba7bfe7b 这篇文章讲了什么是泛型.为什么会有泛型.泛型使用的注意事项. 下面讲点别的内容. 限定符 限 ...
- php限定名称写法,php命名空间:非限定名称、限定名称、完全限定名称实例详解...
在php命名空间中,需要知道关于空间三种名称的术语:非限定名称.限定名称.完全限定名称,以及PHP是怎样解析它们的.官方文档说得非常好,就直接拿来套了,了解它们对学习后面的内容很有帮助.前面了解到命名 ...
- Spring中用@Component、@Repository、@Service和 @Controller等标注的默认Bean名称会是小写开头的非限定类名
今天用调度平台去调用bean中的方法时,提示找不到bean.经查,发现是由于如果在标注上没有提供name属性值,则默认的bean名称是小写开头的,而不是大写开头的. 下面是其他文档参阅: 使用过滤器自 ...
最新文章
- 伪共享 FalseSharing (CacheLine,MESI) 浅析以及解决方案
- c++远征之继承篇——继承方式
- 离线安装 Pytorch 1.2.0 torchvision 0.3.0
- Gprmax 三维地质雷达建模及在 paraview 中的可视化
- 石河子大学计算机学院宿舍,对于那些想去211石河子大学的同学给你一点建议
- java个人中心修改界面怎么整_怎么对个人中心页面访问进行控制
- SQLServer通过链接服务器调用Oracle 存储过程
- oracle sql 取最大分组,oracle sql 按某个字段分组然后从每组取出最大的一条纪录...
- Matlab 嵌套传递函数简化_MATLAB的数据处理方法及图形绘制详解
- 原子变量与非阻塞同步机制
- jvisualvm安装插件出现网络问题
- 用iPhone打造个人的GTD(Get Things Done)实践
- 这是一份值得你去查看的Android安全手册
- UE4TTS文字转语音功能。
- python docx修改word文档格式
- Android自定义IM聊天界面
- 狂神说-Springcloud笔记
- wordpress ajax请求,在wordpress中如何使用ajax
- 中国企业海外征战路线图
- 自从会了Python之后,我就没用过PS了!3秒带你将照片变成素描图片!