1. 集合框架图
Java中的集合是用于存储对象的工具类容器,它实现了常用的数据结构,提供了一系列公开的方法用于增加、删除、修改、查找和遍历数据,降低开发成本。集合种类非常多,形成了一个比较经典的继承关系数,称为Java集合框架图,如下图所示。框架图主要分为两类:第一类按照单个元素存储的Collection,在继承树中Set和List都实现了Collection接口;第二类是按照key-value村村的Map。以上两类集合体系,无论在数据存储还是遍历,都存在非常大的差别。
Java集合框架图.png
在集合框架图中,红色代表接口,蓝色代表抽象类,绿色代表并发包中的类,灰色代表早期线程安全的类(基本已弃用)。可以看到,与Collection相关的4条线分别是List、Set、Queue、Map,它们的子类会映射到数据结构中的表、数、哈希等。
-
List集合
List集合是线性数据结构的主要实现,集合元素通常存在明确的上一个和下一个元素,也存在明确的第一个元素和最后一个元素。List 集合的遍历结果是稳定的。该体系最常用的是ArrayList 和 LinkedList 两个集合类。
ArrayList 是容量可以改变的非线程安全集合。内部实现使用数组进行存储,集合扩客时会创建更大的数组空间,把原有数据复制到新数组中。AmayList 支持对元素的快速随机访问,但是插入与删除时速度通常很慢,因为这个过程很有可能需要移动其它元素。
LinkedList 的本质是双向链表。与 ArrayList 相比,LinkedList 的插入和删除速更快,但是随机访问速度则很慢。测试表明,对于 10万条的数据,与 ArrayList相比随机提取元素时存在数百倍的差距。除继承 AbstractList 抽象类外,LinkedList 还实现了另一个接口 Deque,即 double-ended queue。这个接口同时具有队列和栈的性质。LinkedList 包含3个重要的成员: sizefirst、last。size 是双向链表中节点的个数,first和last分别指向第一个和最后一个节点的引用。LinkedList 的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高。 -
Queue集合
Queue(队列)是一种先进先出的数据结构,队列是一种特殊的线性表,它只许在表的一端进行获取操作,在表的另一端进行插入操作。当队列中没有元素时,称为空队列。自从BlockingQueue(阻塞队列)问世以来,队列的地位得到极大的提升在各种高并发编程场景中,由于其本身 FIFO的特性和阻塞操作的特点,经常被作为Buffer(数据缓冲区)使用。 -
Map集合
Map集合是以Key-Value键值对作为存储元素实现的哈希结构,Key 按某种哈函数计算后是唯一的,Value 则是可以重复的。Map 类提供三种 Collection 视图,集合框架图中,Map 指向 Collection 的箭头仅表示两个类之间的依赖关系。可以使用keySet()查看所有的Key,使用 values()查看所有的Value,使用entrySet()查看所的键值对。最早用于存储键值对的 Hashtable 因为性能瓶颈已经被淘头,而如今广使用的 HashMap,线程是不安全的。ConcurrentHashMap 是线程安全的,在JDK8中进行了锁的大幅度优化,体现出不错的性能。在多线程并发场景中,优先推荐使用ConcurrentHashMap,而不是 HashMap。TreeMap 是 Key 有序的 Map 类集合。 -
Set集合
Set是不允许出现重复元素的集合类型。Set 体系最常用的是 HashSet、TreeSe和LinkedHashSet 三个集合类。HashSet 从源码分析是使用HashMap 来实现的,只是Value固定为一个静态对象,使用 Key 保证集合元素的唯一性,但它不保证集合元素的顺序。TreeSet也是如此,从源码分析是使用 TreeMap 来实现的,底层为树结构,在添加新元素到集合中时,按照某种比较规则将其插入合适的位置,保证插入后的人仍然是有序的。LinkedHashSet 继承自 HashSet,具有 HashSet 的优点,内部使用链表维护了元素插入顺序。
2. 集合初始化
集合初始化通常进行分配客量、设置特定参数等相关工作。我们以使用频率较高为ArayList 和 HashMap 为例,简要说明初始化的相关工作,并解释为什么在任何情况下,都需要显式地设定集合容量的初始大小。ArayList是存储单个元素的顺序表结构,HashMap 是存储 KV 键值对的哈希式结构。分析两者的初始化相关源码,洞悉它们的容量分配、参数设定等相关逻辑,有助于更好地了解集合特性,提升代码质量。下面先从 ArrayList 源码说起:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
// 空表的表示方法
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 值大于 0时,根据构造方法的参数值,忠实地创建一个多大的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 公开的 ada 方法调用此内部私有方法
private void add(E e, Object[] elementData, int s) {
// 当前数组能否容纳 size+1 的元素,如果不够,则调用grow来扩容
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//扩容的最小要求,必须容纳刚才的元素个数 +1,注意,newCapacity()
// 方法才是扩容的重点!
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private Object[] grow() {
return grow(size + 1);
}
private int newCapacity(int minCapacity) {
// overflow-conscious code 防止扩容1.5 倍之后,超过 int 的表示范围(第1处)
int oldCapacity = elementData.length;
// JDK6之前扩容 50%或50-1,但是取ceil,而之后的版本取 Floor (第2处
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//无参数构造方法,会在此时分配默认为10 的容量
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
}
第1处说明:正数带符号右移的值肯定是正值,所以oldCapacity+(oldCapacity>>l)的结果可能超过int可以表示的最大值,反而有可能比参数的 minCapacity 更小,则返回值为(size+1)的minCapacity。
第2处说明:如果原始容量是 13,当新添加一个元素时,依据程序中的计算方法得出13的二进制数为 1101,随后右移1位操作后得到二进制数 110,即十进制数6最终扩容的大小计算结果为 oldCapacitiy +(oldCapacity>>1)= 13+6=19。使用位算主要是基于计算效率的考虑。在JDK7之前的公式,扩容计算方式和结果为 oldCaacitiy x3÷2+1=13x3÷2+1=20。
当ArrayList 使用无参构造时,默认大小为 10,也就是说在第一次 add 的时候分配为10的容量,后续的每次扩容都会调用 Array.copyof方法,创建新数组再复制,可以想象,假如需要将 1000个元素放在 ArayList中,采用默认构造方法,需要被动扩容13次才可以究成存。反之,如果在初始化时便指定了容量new ArrayList(1000),那么在初始化 ArrayList对象的时候就直接分配 1000个储空间而避免被动扩容和数组复制的额外开销。最后,进一步设想,如果这个值达到更大量级,却没有注意初始的容量分配问题,那么无形中造成的性能损耗是非常大的,甚至导致 0OM 的风险。
再来看一下HashMap,如果它需要放置1000个元素,同样没有设置初始容量大小随着元素的不断增加,则需要被动扩客7次才可以完成存储。扩容时需要重建hash表非常影响性能。在 HashMap 中有两个比较重要的参数 Capacity 和 Load Factor,其中Capacity 决定了存储容量的大小,默认为 16;而 Lod Factor 决定了填充比例-般使用默认的0.75。基于这两个参数的乘积,HashMap 内部用 threshold 变量表示HashMap中能放入的元素个数。HashMap 容量并不会在 new 的时候分配,而是在第一次put 的时候完成创建的,源码如下(jdk1.7).
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
.........
}
/**
* Inflates the table. 第一次 put 时,调用如下方法,初始化 table
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
// 找到大于参数值且最接近 2 的幂值,假如输入参数是 27,则返回32
int capacity = roundUpToPowerOf2(toSize);
//threshold 在不超过限制最大值的前提下等于 capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
为了提高运算速度,设定 HashMap 容量大小为2ⁿ,这样的方式使计算落槽位置更快。如果初始化 HashMap 的时侯通过构造器指定了 initialCapacity,则会先计算出比 initialCapacity 大的2 的幂存入 threshold,在第一次 put 时会按照这个2的幂初始化数组大小,此后每次扩容都是增加2倍。如果没有指定初始值,log₂1000 =9.96,结合源码分析可知,如果想要容纳 1000 个元素,必须经过7次扩客。HashMap的扩容还是有不小的成本的,如果提前能够预估出 HashMap 内要放置的元素数量,就可在初始化时合理设置容量大小,避免不断扩容带来的性能损耗。
综上所述,集合初始化时,指定集合初始值大小。如果暂时无法确定集合大小那么指定相应的默认值,这也要求我们记得各种集合的默认值大小,ArayList大小10,而 HashMap 默认值为 16。
3. 数组与集合
数组是一种顺序表,在各种高级语言中,它是组织和处理数据的一种常见方式,我们可以使用索引下标进行快速定位并获取指定位置的元素。数组的下标从0开始,但这并不符合生活常识,这源于BCPL 语言,它将指针设置在0的位置,用数组下标作为直接偏移量进行计算。为什么下标不从1 开始呢?如果是这样,计算偏移量就要使用当前下标减1的操作。加减法运算对 CPU 来说是一种双数运算,在数组下标使用频率极高的场景下,这种运算是十分耗时的。在Java 体系中,数组用以存储同-类型的对象,一旦分配内存后则无法扩容。提倡类型与中括号紧挨相连来定义数组,因为在Java的世界里,万物皆为对象。String[] 用来指代String数组对象,示例代码如下.
String[] strings = {"a", "b"};//数组引用赋值给 Object
Object obj = strings;//使用类名string[]进行强制转化,并成功赋值,strings[0]的值由a变为 object
((String[]) obj)[0] = "object";
声明数组和赋值的方式示例代码如下:
// 初始化完成,容量的大小即等于大括号内元素的个数,使用频率并不高
String[] args3 = {"a", "b"};
String[] args4 = new String[2];
args4[0] = "a";
args4[1] = "h";
上述源码中的 args3 是静态初始化,而 args4 是动态初始化。无论静态初始化还是动态初始化,数组是固定容量大小的。注意在数组动态初始化时,出现了 new,这意味着需要在 new String[]的方括号内填写一个整数。如果写的是负数,并不会编译出错,但运行时会抛出异带:NegsivcAmysizcExoepion。对于动态大小的数组,集合提供了Vector和 AmayLsit 两个类,前者是线程安全,性能校差,基本弃用,而后者是线程不安全,它是使用频率最高的集合之一。
数组的遍历优先推荐 DK5引进的 foreach 方式,即 for(元素:数组名)的方式,可以在不使用下标的情况下遍历数组。如果需要使用数组下标,则使用for(int i=0;i<array.lengt;i++)的方式,注意 length 是数组对象的一个属性,而不是方法。string类是使用 length()方法来获取字符串长度的)。也可以使用JDK8 的函数式口进行遍历:
Arrays.asList(args3).stream().forEach(x-> System.out.println(x));
Arrays.asList(args3).stream().forEach(System.out::println);
Arrays 是针对数组对象进行操作的工具类,包括数组的排序、查找、对比、拷贝等操作。尤其是排序,在多个JDK 版本中在不断地进化,比如原来的归并排序改成Timsort,明显地改善了集合的排序性能。另外,通过这个工具类也可以把数组转成集合。
数组与集合都是用来存储对象的容器,前者性质单一,方便易用;后者类型安全,功能强大,且两者之间必然有互相转换的方式。毕竟它们的性格迥异,在转换过程中,如果不注意转换背后的实现方式,很容易产生意料之外的问题。转换分成两种情况:数组转集合和集合转数组。在数组转集合的过程中,注意是否使用了视图方式直接返回数组中的数据。我们以Arrays.asList0为例,它把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear 方法会抛出UnsupportedOperationException 异常。示例源码如下:
public class ArraysAsList {
public static void main(String[] args) {
String[] stringArray = new String[3];
stringArray[0] = "one";
stringArray[1] = "two";
stringArray[2] = "three";
List<String> stringList = Arrays.asList(stringArray);// 修改转换后的集合,成功地把第一个元素“one”改成“oneList
stringList.set(0, "oneList");
// 运行结果是 oneList,数组的值随之改变
System.out.println(stringArray[0]);
// 这是重点:以下三行编译正确,但都会抛出运行时异常
stringList.add("four");
stringList.remove(2);
stringList.clear();
}
}
事实证明,可以通过set0 方法修改元素的值,原有数组相应位置的值同时也会被修改,但是不能进行修改元素个数的任何操作,否则均会抛出UnsupportedOperationException 异常。Arays.asList 体现的是适配器模式,后台的数据仍是原有数组,set0方法即间接对数组进行值的修改操作。asList 的返回对象是一个Arrays 的内部类,它并没有实现集合个数的相关修改方法,这也正是抛出异常的原因。Arrays.asList 的源码如下:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
返回的明明是ArrayList 对象,怎么就不可以随心所欲地对此集合进行修改呢?注意此ArrayList 非彼ArrayList,虽然Arrays 与ArrayList 同属于一个包,但是在Arrays类中还定义了一个ArrayList的内部类(或许命名为InnerArrayList更容易识别),根据作用域就近原则,此处的ArrayList是李鬼,即这是个内部类。此李鬼十分简单只提供了个别方法的实现,如下所示:
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
// final修饰不准修改其引用 (第1处)
private final E[] a;
// 直接把数组引用赋值给 a,而 objects 是 JDK7引入的工具包
// requireNonNul1 仅仅判断是否为 null
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
// 实现了修改特定位置元素的方法
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
// 注意 set 成功返回的是此位置上的旧值
return oldValue;
}
}
第1处的 final 引用,用于存储集合的数组引用始终被强制指向原有数组。这个内部类并没有实现任何修改集合元麦个数的相关方法,那这个UnspportedOperationException 异常 是 从哪里 出 来的呢? 是李鬼的父类AbstractList:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// clear()方法调用 remove 方法,依然抛出异常
public void clear() {
removeRange(0, size());
}
}
如果李鬼Arrays.ArrayList 内部类覆写这些方法不抛出异常,避免使用者踩进这个坑会不会更好?数组具有不为五斗米折腰的气节,传递的信息是“要么直接用我,要么小心异常!”数组转集合引发的故障还是十分常见的。比如,某业务调用某接口时,对方以这样的方式返回一个 List 类型的集合对象,本方获取集合数据时,99.9%是只读操作,但在小概率情况下需要增加一个元素,从而引发故障。在使用数组转集合时,需要使用李逵iava.util.ArrayList 直接创建一个新集合,参数就是ArraysasList返回的不可变集合,源码如下:
List<Object> objectList = new java.util.ArrayList<Object>(Arrays.asList(stringArray));
相对于数组转集合来说,集合转数组更加可控,毕竟是从相对自由的集合容器转为更加苛刻的数组。什么情况下集合需要转成数组呢?适配别人的数组接口,或者进行局部方法计算等。先看一个源码,猜猜执行结果
public class ListToArray {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(3);
list.add("one");
list.add("two");
list.add("three");
//泛型丢失,无法使用 string[] 接收无参方法返回的结果 (第1处)
Object[] arrayl = list.toArray();
// array2 数组长度小于元素个数 (第2处)
String[] array2 = new String[2];
list.toArray(array2);
System.out.println(Arrays.asList(array2));
// array3 数组长度等于元素个数 (第3处)
String[] array3 = new String[3];
list.toArray(array3);
System.out.println(Arrays.asList(array3));
}
}
执行结果如下:
[null,null]
[one,two, three]
第1处比较容易理解,不要用toArra()无参方法把集合转换成数组,这样会致泛型丢失;在第2处执行成功后,输出却为 null;第3处正常执行,成功地把集合数据复制到array3数组中。第2处与第3处的区别在于即将复制进去的数组容量是否足够。如果容量不够,则弃用此数组,另起炉灶,关于此方法的源码如下.
// 注意入参数组的 length 大小是重中之重,如果大于或等于集合的大小
// 则集合中的数据复制进入数组即可,如果空间不够,入参数组 a 就会被无视
// 重新分配一个空间,复制完成后返回一个新的数组引用
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
// 如果数组长度小于集合 size,那么执行此语句,直接 return。(第1处)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 如果容量足够,则直接复制 (第2处)
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
// 只有在数组容量足够的情况下,才返回传入参数
return a;
}
第1处和第 2 处均 复制 java.util.ArrayLit 的 elementData到数组中,这个elementData是 ArrayList 集合对象中真正用于存储数据的数组,它的定义为:transient Object[] elementData
这个存储ArrayList 真正数据的数组由 transient 修饰,表示此字段在类的序列化时将被忽略。因为集合序列化时系统会调用 writeOtject 写入流中,在网络客户端反序列化的readObject 时,会重新赋值到新对象的 elementData 中。为什么多此一举?因为 elementData 容量经常会大于实际存储元素的数量,所以只需发送真正有实际值的数组元素即可。回到刚才的场景,当入参数组客量小于集合大小时,使用Amsys.copy0f()方法,它的源码如下
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
// 新创建一个数组 copy
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
如果数组初始大小设置不当,不仅会降低性能,还会浪费空间。使用集合的toArray(T[] array)方法,转换为数组时,注意需要传入类型完全一样的数组,并且的容量大小为 list.size()。
4. 集合与泛型
泛型与集合的联合使用,可以把泛型的功能发挥到极致,很多小伙伴不清楚List、List<Object>、List<?> 三者的区别,更加不能区分<? extends T> 与<? superT的使用场景。List 完全没有类型限制和赋值限定,如果天马行空地乱用,迟早会遭类型转换失败的异常。很多程序员觉得 List<Objec> 的用法完全等同于 List,但在接受其他泛型赋值时会编译出错。List<?> 是一个泛型,在没有赋值之前,表示它可以接受任何类型的集合赋值,值之后就不能便往里添加元素了。下方的例不很好活明了三者的区别,以List为原型展开说明:
public class ListNoGeneric {
public static void main(String[] args) {
// 第一段:泛型出现之前的集合定义方式
List a1 = new ArrayList();
a1.add(new Object());
a1.add(new Integer(111));
a1.add(new String("hello alal"));
//第二段:把a1引用赋值给 a2,注意 a2与al的区别是增加了泛型原制<opject>
List<Object> a2 = a1;
a2.add(new Object());
a2.add(new Integer(222));
a2.add(new String("hello a2a2"));
//第三段:把al引用赋值给 a3,注意a3与a1的区别是增加了泛型<Integer>
List<Integer> a3 = a1;
a3.add(new Integer(333));
//下方两行编译出错,不允许增加非 Integer 类型进入集合
a3.add(new Object());
a3.add(new String("hello a3a3"));
// 第四段:把a1 引用赋值给 a4,a1 与a4的区别是增加了通配符
List<?> a4 = a1;
// 允许删除和清除元素
a1.remove(0);
a4.clear();
// 编译出错。不允许增加任何元素
a4.add(new Object());
}
}
第一段说明:在定义 List 之后,毫不犹豫地往集合里装入三种不同的对象:Object、Integer 和 String,遍历没有问题,但是贸然以为里边的元素都是 Integer,使用强制转化,则抛出 ClassCastException 异常。
第二段说明:把 a1 赋值给 a2,a2 是 List<Objec> 类型的,也可以再往里装入三种不同的对象。很多程序员认为 List 和 List<Objec> 是完全相同的,至少从目前这两段来看是这样的。
第三段说明:由于泛型在JDK5 之后才出现,考虑到向前兼客,因此历史代码有时需要赋值给新泛型代码,从编译器角度是允许的。这种代码似乎有点反人类,在实际故障案例中经常出现,来看一段问题代码。
JsoNobject jsonobject = JSoNobject.fromobject ("(\"level\":[\"3 \"])"):
List<Integer> intList= new ArrayList<Integer>(10);
if (jsonObject != nul1) {
intList.addAll(jsonObject.getJSONArray("level"));
int amount=0;
for (Integer t : intList) (
//抛出classCastException异带 : string cannot be cast to Integer
if (condition) {
amount = amount + t;
}
}
}
addAll的定义如下:
public boolean addAll(Collection<? extends E> c) {...]
进行了泛型限制,示例中addAll的实际参数是getJSONArray 返回的JSONArray对象,它并非是List,更加不是Imteger集合的子类,为何编译不报错?查看JSONArray 的定义:
public final class JSONArray extends AbstractJSON implements JSON, List {}
JSONArray 实现了 List,是非泛型集合,可以赋值给任何泛型限制的集合。编译可以通过,但在运行时报错,这是一个隐藏得比较深的Bug,最终导致发生线上故障。在JDK5 之后,应尽量使用泛型定义,以及使用类、集合、参数等。
如果把al的定义从List a1修改为 List<Object>a1,那么第三段就会编译出错List<Objec> 赋值给 List<Integer> 是不允许的,若是反过来赋值:
List<Integer> intList = new ArrayList<Integer>(3);
intList.add(111);
List<Object> objectlist = intList;
事实上,依然会编译出错,提示如下:
Error:(10, 26) java: incompatible types: java.util.List<java.lang.Integer> cannot be converted tojava.util.List<java.lang.Object>
注意,数组可以这样赋值,因为它是协变的,而集合不是。
第四段说明:间号在正则表达式中可以匹配任何字符,List<?>称为通配待集合可以接受任何类型的集合引用赋值,不能添加任何元素,但可以remove和clear,并非 immutable 集合。List<?>一般作为参数来接收外部的集合,或者返回一个不知具体元素类型的集合。
List<T>最大的问题是只能放置一种类型,如果随意转换类型的话,就是“破窗像论”,泛型就失去了类型安全的意义。如果需要放置多种受泛型约束的类型呢?JDK 的开发者顺应了民意,实现了 <? extends T>与<? super>两种语法,但是两的区别非常微妙。简单来说,<?extends T>是 Get First,适用于,消费集合元素为主的场景,<?super T>是 Put First,适用于,生产集合元素为主的场景。
<? extends T>可以赋值给任何T及T子类的集合,上界为T,取出来的类型带有泛型限制,向上强制转型为 T。null 可以表示任何类型,所以除 ull外,任何元素都不得添加进<?extends T>集合内。
<? super T>可以赋值给任何T及T的父类集合,下界为 T。在生活中,投票选举类似于<?super T>的操作。选举代表时,你只能往里投选票,取数据时,根本不知道是谁的票,相当于泛型丢失。有人说,这只是一种生活场景,在系统设计中,很难有这样的情形。再举例说明一下,我们在填写对主管的年度评价时,提交后若想再次访问之前的链接修改评价,就会被告之:“您已经完成对主管的年度反馈,谢谢参与。”extends的场景是put 功能受限,而 super的场景是get功能受限。
下例中,以加菲猫、猫、动物为例,说明 extends和super的详细语法差异:
public class AnimalCatGarfield {
public static void main(String[] args) {
//第1段;声明三个依次承的类的集合: Object>动物>猫>加菲猫
List<Animal> animal = new ArrayList<Animal>();
List<Cat> cat = new ArrayList<Cat>();
List<Garfield> garfield = new ArrayList<Garfield>();
animal.add(new Animal());
cat.add(new Cat());
garfield.add(new Garfield());
//第二段测试赋值操作
// 下行编译出错。只能赋值 Cat 或 cat 子类的集合
List<? extends Cat> extendsCatFromAnimal = animal;
List<? super Cat> superCatFromAnimal = animal;
List<? extends Cat> extendsCatFromCat = cat;
List<? super Cat> superCatFromCat = cat;
List<? extends Cat> extendsCatFromGarfield = garfield;
//下行编译出错。只能制值Cat或Cat父类的集合
List<? super Cat> superCatFromGarfield = garfield;
//第3段:测试add 方法
// 下面三行中所有的<? extends T> 都无法进行add操作,编译均出错
extendsCatFromCat.add(new Animal());
extendsCatFromCat.add(new Cat());
extendsCatFromCat.add(new Garfield());
// 下行编译出错。只能添加 cat 或 Ca 子类的集合
superCatFromCat.add(new Animal());
superCatFromCat.add(new Cat());
superCatFromCat.add(new Garfield());
//第4段:测试get 方法
// 所有的 super 操作能够返回元素,但是泛型丢失,只能返回 object 对象
//以下extends 操作能够返回元素
Object catExtends2 = extendsCatFromCat.get(0);
Cat catExtends1 = extendsCatFromCat.get(0);
// 下行编译出错。虽然 Cat 集合从 Garfield 赋值而来,但类型擦除后,是不知道的
Garfield garfield1 = extendsCatFromGarfield.get(0);
}
}
第1段,声明三个泛型集合,可以理解为三个不同的笼子,List<Anima>住的是动物(反正就是动物世界里的动物),List<Ca住的是猫(反正就是猫科动物),List<Garfield>住的是加菲猫(又懒又可爱的一种猫)。Garfield 继承于Cat,而Ca继承自Animal。
第2段,以Cat 类为核心,因为它有父类也有子类。定义类型限定集合,分别为 List<? extends Cat>和List<? super Cat>。在理解这两个概念时,暂时不要引入上界和下界,专注于代码本身就好。
把 List<Cat> 对象赋值给两者都是可以的。但是把 List<Animal> 赋值给 List<? extends Cat> 时会编译出错,因为能赋值给 <? extend Cat> 的类型,只有 Cat 自己和它的子类集合。尽管它是类型安全的,但依然有泛型信息,因而从笼子里取出来的必然是只猫,而List<Animal>里边有可能住着毒蛇、鳄鱼蝙蝠等其他动物。把 List<Garfield> 赋值给 List<? super Cat> 时,也会编译报错。因为能赋值给<?super Cat>的类型,只有 Cat自己和它的父类。
第3段,所有的 List<?extends T>都会编译出错,无法进行add 操作,这是因为除 null外,任何元素都不能被添加进<? extends T> 集合内。List<? super Cat> 可以往里增加元素,但只能添加Cat 自身及子类对象,假如放入一块石头,则明显违背了Animal大类的性质。
第4段,所有 List<? super T> 集合可以执行 get操作,虽然能够返回元素,但是类型丢失,即只能返回Object 对象。List<?extends Cat>可以返回带类型的元素,但只能返回 Cat 自身及其父类对象,因为子类类型被擦除了。
对于一个笼子,如果只是不断地向外取动物而不向里放的话,则属于 Get First,应采用<?extends T>;相反,如果经常向里放动物的话,则应采用<? super T>,属于Put First。
网友评论