【Java 泛型】之 <? super T> 和<? extends T> 中 super ,extends如何理解?有何异同?
Java 泛型 <? super T> 和<? extendsT>中 super ,extends怎么 理解?有何不同?
简介
前两篇文章介绍了泛型的基本用法、类型擦除以及泛型数组。在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用。
这个系列的另外两篇文章链接如下:
数组的协变
在了解通配符之前,先来了解一下数组。Java 中的数组是协变的,什么意思?看下面的例子:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~
main
方法中的第一行,创建了一个 Apple
数组并把它赋给 Fruit
数组的引用。这是有意义的,Apple
是 Fruit
的子类,一个 Apple
对象也是一种 Fruit
对象,所以一个 Apple
数组也是一种 Fruit
的数组。这称作数组的协变,Java 把数组设计为协变的,对此是有争议的,有人认为这是一种缺陷。
尽管 Apple[]
可以 “向上转型” 为 Fruit[]
,但数组元素的实际类型还是 Apple
,我们只能向数组中放入 Apple
或者 Apple
的子类。在上面的代码中,向数组中放入了 Fruit
对象和 Orange
对象。对于编译器来说,这是可以通过编译的,但是在运行时期,JVM 能够知道数组的实际类型是 Apple[]
,所以当其它对象加入数组的时候就会抛出异常。
泛型设计的目的之一是要使这种运行时期的错误在编译期就能发现,看看用泛型容器类来代替数组会发生什么:
// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();
上面的代码根本就无法编译。当涉及到泛型时, 尽管 Apple
是 Fruit
的子类型,但是 ArrayList<Apple>
不是 ArrayList<Fruit>
的子类型,泛型不支持协变。
使用通配符
从上面我们知道,List<Number> list = ArrayList<Integer>
这样的语句是无法通过编译的,尽管 Integer
是 Number
的子类型。那么如果我们确实需要建立这种 “向上转型” 的关系怎么办呢?这就需要通配符来发挥作用了。
上边界限定通配符
利用 <? extends Fruit>
形式的通配符,可以实现泛型的向上转型:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can’t add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
上面的例子中, flist
的类型是 List<? extends Fruit>
,我们可以把它读作:一个类型的 List, 这个类型可以是继承了 Fruit
的某种类型。注意,这并不是说这个 List 可以持有 Fruit
的任意类型。通配符代表了一种特定的类型,它表示 “某种特定的类型,但是 flist
没有指定”。这样不太好理解,具体针对这个例子解释就是,flist
引用可以指向某个类型的 List,只要这个类型继承自 Fruit
,可以是 Fruit
或者 Apple
,比如例子中的 new ArrayList<Apple>
,但是为了向上转型给 flist
,flist
并不关心这个具体类型是什么。
如上所述,通配符 List<? extends Fruit>
表示某种特定类型 ( Fruit
或者其子类 ) 的 List,但是并不关心这个实际的类型到底是什么,反正是 Fruit
的子类型,Fruit
是它的上边界。那么对这样的一个 List 我们能做什么呢?其实如果我们不知道这个 List 到底持有什么类型,怎么可能安全的添加一个对象呢?在上面的代码中,向 flist
中添加任何对象,无论是 Apple
还是 Orange
甚至是 Fruit
对象,编译器都不允许,唯一可以添加的是 null
。所以如果做了泛型的向上转型 (List<? extends Fruit> flist = new ArrayList<Apple>()
),那么我们也就失去了向这个 List 添加任何对象的能力,即使是 Object
也不行。
另一方面,如果调用某个返回 Fruit
的方法,这是安全的。因为我们知道,在这个 List 中,不管它实际的类型到底是什么,但肯定能转型为 Fruit
,所以编译器允许返回 Fruit
。
了解了通配符的作用和限制后,好像任何接受参数的方法我们都不能调用了。其实倒也不是,看下面的例子:
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is ‘Object’
flist.indexOf(new Apple()); // Argument is ‘Object’
//flist.add(new Apple()); 无法编译
}
}
在上面的例子中,flist
的类型是 List<? extends Fruit>
,泛型参数使用了受限制的通配符,所以我们失去了向其中加入任何类型对象的例子,最后一行代码无法编译。
但是 flist
却可以调用 contains
和 indexOf
方法,它们都接受了一个 Apple
对象做参数。如果查看 ArrayList
的源代码,可以发现 add()
接受一个泛型类型作为参数,但是 contains
和 indexOf
接受一个 Object
类型的参数,下面是它们的方法签名:
public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)
所以如果我们指定泛型参数为 <? extends Fruit>
时,add()
方法的参数变为 ? extends Fruit
,编译器无法判断这个参数接受的到底是 Fruit
的哪种类型,所以它不会接受任何类型。
然而,contains
和 indexOf
的类型是 Object
,并没有涉及到通配符,所以编译器允许调用这两个方法。这意味着一切取决于泛型类的编写者来决定那些调用是 “安全” 的,并且用 Object
作为这些安全方法的参数。如果某些方法不允许类型参数是通配符时的调用,这些方法的参数应该用类型参数,比如 add(E e)
。
当我们自己编写泛型类时,上面介绍的就有用了。下面编写一个 Holder
类:
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns ‘Object’
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
在 Holer
类中,set()
方法接受类型参数 T
的对象作为参数,get()
返回一个 T
类型,而 equals()
接受一个 Object
作为参数。fruit
的类型是 Holder<? extends Fruit>
,所以set()
方法不会接受任何对象的添加,但是 equals()
可以正常工作。
下边界限定通配符
通配符的另一个方向是 “超类型的通配符“: ? super T
,T
是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
writeTo
方法的参数 apples
的类型是 List<? super Apple>
,它表示某种类型的 List,这个类型是 Apple
的基类型。也就是说,我们不知道实际类型是什么,但是这个类型肯定是 Apple
的父类型。因此,我们可以知道向这个 List 添加一个 Apple
或者其子类型的对象是安全的,这些对象都可以向上转型为 Apple
。但是我们不知道加入 Fruit
对象是否安全,因为那样会使得这个 List 添加跟 Apple
无关的类型。
在了解了子类型边界和超类型边界之后,我们就可以知道如何向泛型类型中 “写入” ( 传递对象给方法参数) 以及如何从泛型类型中 “读取” ( 从方法中返回对象 )。下面是一个例子:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src)
{
for (int i=0; i<src.size(); i++)
dest.set(i,src.get(i));
}
}
src
是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:<? extends T>
,取出的元素转型为 T
。dest
是要写入的目标 List,所以用了下边界限定通配符:<? super T>
,可以写入的元素类型是 T
及其子类型。
无边界通配符
还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List<?>
,也就是没有任何限定。不做任何限制,跟不用类型参数的 List
有什么区别呢?
List<?> list
表示 list
是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list
,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object
,因此可以添加任何类型的对象,只不过编译器会有警告信息。
总结
通配符的使用可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:
使用
List<? extends C> list
这种形式,表示 list 可以引用一个ArrayList
( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素类型是C
的子类型 ( 包含C
本身)的一种。使用
List<? super C> list
这种形式,表示 list 可以引用一个ArrayList
( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素就类型是C
的超类型 ( 包含C
本身 ) 的一种。
大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。
摘自:http://www.cnblogs.com/hoojjack/p/6817547.html?utm_source=itdadao&utm_medium=referral
【Java 泛型】之 <? super T> 和<? extends T> 中 super ,extends如何理解?有何异同?的更多相关文章
- java 中 this 和 super 说明及在构造器中super()和this()相互调用执行顺序
this this 表示当前对象 使用上细分的话,this有 this. 和this()的使用情况 ,下面我们开始细撸 this . 使用场景一: 在成员方法中,this.变量名 指带当前对象的变量, ...
- Java深度历险(五)——Java泛型
作者 成富 发布于 2011年3月3日 | 注意:QCon全球软件开发大会(北京)2016年4月21-23日,了解更多详情!17 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件 ...
- java 深度探险 java 泛型
Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JD ...
- Java 泛型 泛型代码和虚拟机
Java 泛型 泛型代码和虚拟机 @author ixenos 类型擦除.原始类型.给JVM的指令.桥方法.Java泛型转换的事实 l 类型擦除(type erasure) n Java泛型的处理 ...
- java泛型使用教程
参考: java 泛型 Java泛型中E.T.K.V等的含义 一.Java泛型中E.T.K.V等的含义 E - Element (在集合中使用,因为集合中存放的是元素) T - Type(Jav ...
- Java-Runoob-高级教程:Java 泛型
ylbtech-Java-Runoob-高级教程:Java 泛型 1.返回顶部 1. Java 泛型 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检 ...
- java泛型使用
泛型的解释 现在感觉泛型是一个值得学习的地方,就抽出时间来学习和总结一下泛型的使用. Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允 ...
- Java 学习(17): Java 泛型
Java 泛型 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说将 ...
- JAVA 泛型中的通配符 T,E,K,V,?
前言 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据 ...
随机推荐
- 【Java】数组Array
Java基础复习之:数组 简介 数组(Array):多个相同数据类型按照一定顺序排列的集合,并使用一个名字命名,通过编号的方式对这些数据进行统一管理 一维数组 一维数组的声明与初始化 int[] id ...
- 由于xftp打开target目录,导致maven编译的时候target目录无法访问,打包失败
由于xftp打开target目录,导致maven编译的时候target目录无法访问,打包失败: 在xftp里关闭target目录就可以了...无时不在的坑
- pyhon-高并发测试
使用gevent.对注册.接口进行200的并发量进行测试. #!/usr/bin/env python # -*- coding:utf-8 -*- #设置路径:Defualt Settings--- ...
- jenkins bat删除指定路径下的文件及文件夹
最近在用jenkins集成,生成allure测试报告,但是每次生成的allure测试报告,都是上一次执行的痕迹.比如这次我只运行了100个用例,结果显示运行2000条,上一次运行的用例,时间也涵括了上 ...
- [bzoj3351]Regions
这道题有一种较为暴力的做法,对于每个点枚举所有与r2为该属性的询问并加以修改,最坏时间复杂度为o(nq),然而是可过的(97s) 发现只有当r2相同的询问数特别多时才会达到最坏时间复杂度,因此如果删除 ...
- [atARC103D]Robot Arms
合法的必要条件是每个点两维坐标和奇偶性相同,同时这也是充分条件 令$d_{i}=\{2^{0},2^{1},...,2^{m-1}\}$,归纳其可以走到任意满足$|x|+|y|<2^{m}$的$ ...
- Scrum精髓读书笔记
Scrum精髓 四 . Sprint Sprint的定义 Scrum在最长一个月的迭代或周期中安排工作,一般为2个星期,这些迭代或周期称为Sprint Sprint提供基本的Scrum骨架,大多数其他 ...
- C/C++ Qt TreeWidget 单层树形组件应用
TreeWidget 目录树组件,该组件适用于创建和管理目录树结构,在开发中我们经常会把它当作一个升级版的ListView组件使用,因为ListView每次只能显示一列数据集,而使用TableWidg ...
- SpringServletContainerInitializer的代码流程
SpringServletContainerInitializer 是spring中的一个class实现了servlet3.0规范的一个接口 implements ServletContainerIn ...
- 从零开始学Kotlin第四课
面向对象: //妹子 性格 声音 class Girl(var chactor:String,var voice:String) fun main(args: Array<String>) ...