【Java】java 中的泛型通配符——从“偷偷地”地改变集合元素说起
一直没注意这方面的内容,想来这也算是基础了,就写了这个笔记。
首先java的通配符共有三种————先别紧张,现在只是粗略的过一下,看不看其实无所谓
| 类型 | 介绍 |
|---|---|
| <?> | 无限定通配符,等价于 <? extends Object> |
| <? extends Number> | 上限通配符,表示参数类型只能是 Number 或是 Number 的子类。 |
| <? super Number> | 下限通配符,表示参数类型只能是 Number 或是 Number 的父类。 |
然后再让我们定义四个类,下面会用到
class A {
public String getName() {
return "A";
}
}
class B extends A{
@Override
public String getName() {
return "B";
}
}
class BAge extends B{
@Override
public String getName() {
return "C";
}
public int getAge() {
return 100;
}
}
class BSize extends B{
@Override
public String getName() {
return "D";
}
public int getSize() {
return -1;
}
}
从一个奇怪的现象说起
- 首先,我们再引入一个类 PrintAges ,用于打印 BAge 的 getAge()
class PrintAges{
public static void print(BAge[] ages){
if (ages == null)
return;
for (BAge bage : ages){
if (bage != null)
System.out.println(bage.getAge());
}
}
}
仔细看看上面这个类,你觉得我写的 PrintAges 怎样?够完美吗,不会引发异常吧?我觉得也很完美了,肯定不会有异常出现在我的代码里了。
- 我们测试下
BAge[] temps = new BAge[]{new BAge(), new BAge()};
PrintAges.print(temps);
输出:
100
100
完美运行。
- 我们再增加两行
BAge[] temps = new BAge[]{new BAge(), new BAge()};
B[] barray = temps; // 新增加的第一行
barray[0] = new BSize(); // 新增加的第二行
PrintAges.print(temps);
你猜怎么着?我偷偷地改变了数组中的元素!我在 BAge 类型的数组中的元素赋了一个 BSize 的对象!
而且,编译通过了。但是肯定会有异常出现,你猜是在哪一行?
输出:
Exception in thread "main" java.lang.ArrayStoreException: JavaApp.BSize at JavaApp.JavaApplicationStudyGen.main(JavaApplicationStudyGen.java:33)
本来我以为会在 PrintAges 的 print 方法中发生异常,但是实际上新增加的第二行发生了运行时错误,赋值错误。
而在C#中,这种问题出现的可能性就更小了。C#中,新增的第一行是无法通过编译的。
那么,这种问题在集合……准确地说是在泛型里会不会出现呢?
- 上述问题在泛型中的讨论。
我们先对 PrintAges 添加一个 print 函数的重载
class PrintAges{
public static void print(ArrayList<BAge> list) {
if (list == null)
return;
for(BAge age : list) System.out.println(age.getAge());
}
public static void print(BAge[] ages){
if (ages == null)
return;
for (BAge bage : ages){
if (bage != null)
System.out.println(bage.getAge());
}
}
}
然后我们对用再次运行如下代码:
ArrayList<BAge> list = new ArrayList<BAge>();
list.add(new BAge());
ArrayList<B> yourList = list; // 编译错误
yourList.set(0, new BSize()); // star 1
BAge age = list.get(0); // star 2
PrintAges.print(list);
这次,Java 处理的比较严格,在把 ArrayList<BAge> 赋值给 ArrayList<B> 类型的对象时产生了编译错误。
在 C# 里,也是一样的,在把 ArrayList<BAge> 赋值给 ArrayList<B> 类型的对象时会产生编译错误。
一开始,我不理解这样做对 list 引用的对象 ArrayList 会产生什么负面影响。
但是,不能赋值的原因,把一个 BSize 类型的对象放在了一个实际上是 ArrayList 的集合里。而ArrayList 又假设集合中的元素类型都是 BAge 。倒不是运行时的虚拟机会假设,因为泛型最后都会类型擦除(type erasure)。其实倒不是类型擦除本身引起了这个错误,而是本来就存在这样一种现象。我给出类型擦除之后的样子是为了便于理解。
经过类型擦除之后, star 2 所在行的代码就会变成
BAge age = (BSize)list.get(0); // star 2
这样就是完全不正确的了。
也就是说,我们应该禁止类似 ArrayList<B> yourList = new ArrayList<BAge>() 这样的赋值,否则,就会出现这样的错误和意外。
说实话,B[] barray = new BAge[]{new BAge(), new BAge()} 这样的赋值操作也该被禁止的,但是 Java 就可以。看看人家 C# 就不允许这样做(笑)
记住这样的错误。接下来,我们就可以讨论 Java 的泛型通配符了。
通配符出现的原因
所以所,通配符的出现就是为了在错误避免上述错误的同时,给程序员提供一点便利。
而通配符是怎么样发生作用的呢?是通过编译器给定的三条“游戏规则”(也即是上面给的表格里的规则)发生作用的。
在一开始理解的时候是需要一点逻辑能力的:
- 上限通配符
<? extends B>确保了可读性,<? extends B> 表示参数类型只能是 B 或是 B 的子类可以被编译通过的语句:
ArrayList<? extends B> list = new ArrayList<A>(); // 编译错误
ArrayList<? extends B> list = new ArrayList<B>(); // ok
ArrayList<? extends B> list = new ArrayList<BAge>(); // ok
ArrayList<? extends B> list = new ArrayList<BSize>(); // ok
基于以上的编译规则,我们可以得出以下事实:
- 你一定能从 list 中读取到一个 B 元素,因为 list 要么指向
ArrayList<B>,要么指向包含 B 子类对象的ArrayList<B> - 你不能不能插入一个 B 元素 ,因为 list 可能指向的是
ArrayList<BSize>或者指向ArrayList<BAge> - 你不能不能插入一个 BAge 元素 ,因为 list 可能指向的是
ArrayList<BSize> - 你不能不能插入一个 BSize 元素 ,因为 list 可能指向的是
ArrayList<BAge>
注意,上述代码中, list 中的 T 被替换成了 ? extends B
也就是说,读取操作可以被确保,你一定能从 list 中读取到一个 B 元素 这样, list.get 方法就可以被正常使用了。
而 list.set(int, T) 就被替换成了 list.set(int, ? extends B),这个方法就被编译器“禁止”了。也就是说,如果你写出 list.set(0, new B()) 或 list.set(0, new BSize()) 是不行的。
在这里你肯定要提出疑问了,你不是说符合“游戏规则” <? extends B> 表示参数类型只能是 B 或是 B 的子类 就行的吗? 我只能说,文字所能传达的信息是有限的,这个表述也只适用于 ArrayList<? extends B> list = new ArrayList<A>(); 这样的赋值时刻。还是得看上述推导的“事实”
- 下限通配符
<? super B>确保了写入性
ArrayList<? super B> list = new ArrayList<Object>(); // ok
ArrayList<? super B> list = new ArrayList<A>(); // ok
ArrayList<? super B> list = new ArrayList<B>(); // ok
ArrayList<? super B> list = new ArrayList<BAge>(); // 编译错误
ArrayList<? super B> list = new ArrayList<BSize>(); // 编译错误
基于以上的编译规则,我们可以得出以下事实:
- 你一定能插入一个 B 类型的对象或者 B 子类型的对象。因为, list 要么指向包含 B 类型的
ArrayList,要么指向包含 B 超类型的 ArrayList 对象,比如: list 可能是ArrayList<Object>或ArrayList<A>。 - 你一定你不能保证读取到 B ,因为 list 可能指向
ArrayList<Object>或者是ArrayList<B>
这样, list.set 方法就可以被正常使用了。假设 list 指向 ArrayList<Object> ,我们把一个 B 类型的对象添加到 ArrayList<Object> 中也没错啊。
- 或者,我们把一个
BAge对象添加到ArrayList<Object>或ArrayList<A>中也没错啊。 - 或者,我们把一个
BSize对象添加到ArrayList<Object>或ArrayList<A>中也没错啊。
总结
- 通配符的出现是为了让程序员在避免上述错误的情况下能放宽一点要求,即所谓的“符合我编译器的规则,就让你舒服”
? extends B确保了可读性,? super B确保了写入性。? extends B和? super B给人的感觉是逆操作。
【Java】java 中的泛型通配符——从“偷偷地”地改变集合元素说起的更多相关文章
- Java修炼——ArrayList常用的方法以及三种方式遍历集合元素。
List接口ArrayList用法详解 ArrayList常用方法: 1. List.add():添加的方法(可以添加字符串,常量,以及对象) List list=new ArrayList(); l ...
- 简谈 Java 中的泛型通配符
很好的一篇文章https://zhuanlan.zhihu.com/p/26681625
- Java那些事-泛型通配符
Java的类型通配符,可以出现在类.方法上面.最常用的方式就是集合类,例如List,Set等类上面. 通配符类型 有泛型参数 List 有无类型标识 List< ? > 有通用的标识 Li ...
- java中的泛型(转)
什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个 ...
- 对Java通配符的个人理解(以集合为例)
对Java通配符的个人理解(以集合为例) 前言:最近在学习Java,当学到了泛型的通配符时,不是很理解PECS(Producer Extends Consumer Super)原则,以及<? e ...
- java学习中碰到的疑惑和解答(一)
今天写一个接口的时候发现,接口的方法不需要写修饰符,直接写数据类型加上方法名(参数)即可通过编译. import java.util.List; import com.bjm.pojo.Flower; ...
- Java开发中的23种设计模式详解(3)行为型
本章是关于设计模式的最后一讲,会讲到第三种设计模式--行为型模式,共11种:策略模式.模板方法模式.观察者模式.迭代子模式.责任链模式.命令模式.备忘录模式.状态模式.访问者模式.中介者模式.解释器模 ...
- .NET中的泛型概述
什么是泛型? 泛型是具有占位符(类型参数)的类.结构.接口和方法,这些占位符是类.结构.接口和方法所存储或使用的一个或多个类型的占位符.泛型集合类可以将类型形参用作其存储的对象类型的占位符:类型形参呈 ...
- java中的泛型【T】与通配符【?】概念入门
使用泛型的目的是利用Java编译机制,在编译过程中帮我们检测代码中不规范的有可能导致程序错误的代码.例如,我们都知道List容器可以持有任何类型的数据,所以我们可以把String和Integer等类型 ...
随机推荐
- C#常用的字符串处理方法
1.Replace(替换字符):public string Replace(char oldChar,char newChar);在对象中寻找oldChar,如果寻找到,就用newChar将oldCh ...
- JavaScript中的面向对象程序设计
本文内容目录顺序: 1.Object概念讲述: 2.面向对象程序设计特点: 3.JavaScript中类和实例对象的创建: 4.原型概念: 5.原型API: 6.原型对象的具体使用:7.深入理解使用原 ...
- hibernate利用mysql的自增张id属性实现自增长id和手动赋值id并存
我们知道在mysql中如果设置了表id为自增长属性的话,insert语句中如果对id赋值(值没有被用到过)了,则插入的数据的id会为用户设置的值,并且该表的id的最大值会重新计算,以插入后表的id最大 ...
- [LeetCode] Reverse Pairs 翻转对
Reverse Pairs 翻转对 题意 计算数组里面下标i小于j,但是i的值要大于j的值的两倍的搭配的个数(也就是可能会有多种搭配):网址 做法 这道题显然是不允许使用最简单的方法:两次循环,逐次进 ...
- Charles从入门到放弃
Charles版本:4.0.2 一.开始 连接方式 方法一:电脑和手机连接同一个wifi 方法二:电脑使用网线连接网络,手机通过USB连接电脑 二.过滤网络请求 1.简单过滤 在Sequence模式下 ...
- 使用SimpleXML解析xml文件数据
最近工作要求从一个XML文档中批量读取APK应用数据,自然想到用SimpleXML.经过一段时间摸索,终于成功解析,现在将思路以及代码做下记录: xml文件格式大致如下: <?xml versi ...
- C++获取基类指针所指子类对象的类名
我们在程序中定义了一个基类,该基类有n个子类,为了方便,我们经常定义一个基类的指针数组,数组中的每一项指向都指向一个子类,那么在程序中我们如何判断这些基类指针是指向哪个子类呢? 关键字 typeid, ...
- Ubuntu下比较通用的makefile实例
本文转自http://blog.chinaunix.net/uid-20608849-id-360294.html 笔者在写程序的时候会遇到这样的烦恼:一个项目中可能会有很多个应用程序,而新建一个应 ...
- Can you answer these queries?
Can you answer these queries? Time Limit:2000MS Memory Limit:65768KB 64bit IO Format:%I64d & ...
- 一款超好用轻量级JS框架——Zepto.js(上)
前 言 絮叨絮叨 之前我们介绍过JQuery怎么自定义一个插件,但没有详细介绍过JQuery,那么今天呢....我们还是不说JQuery,哈哈哈哈 但是今天我们介绍一款和JQuery超级像的一 ...