为了使具体集合的定义更加简单和规范,Java集合框架中给出了一套抽象基类,只要继承该抽象基类就能轻松定义出自己的具体集合类,其中抽象基类包括:
AbstractCollection
AbstractList
AbstractQueue
AbstractSequentialList
AbstractMap
AbstractSet
注意:AbstractQueue的实现类都在java.util.concurrent中,所以这里暂不考虑,后面再专门学习concurrent包的内容。
一、AbstractCollection
首先给出AbstractCollection的定义:
public abstract class AbstractCollection<E> implements Collection<E> {
protected AbstractCollection() //构造函数
public String toString() //String形式
public abstract Iterator<E> iterator(); //迭代器
public abstract int size(); //集合大小
public boolean isEmpty() //是否为空
public Object[] toArray() //转换为数组
public <T> T[] toArray(T[] a) //转换为T类型数组
private static <T> T[] finishToArray(T[] r, Iterator<?> it) //完成转换
private static int hugeCapacity(int minCapacity) //获取最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //最大数组容量
public boolean add(E e)
public boolean remove(Object o)
public boolean contains(Object o)
public void clear()
public boolean addAll(Collection<? extends E> c)
public boolean removeAll(Collection<?> c)
public boolean containsAll(Collection<?> c)
public boolean retainAll(Collection<?> c)
}
总体来说,AbstractCollection是个抽象程度比较高的集合抽象类,它既没有定义元素的存储方式也没有定义元素的增加方式,仅实现了主要靠迭代器遍历能达成的方法,比如删除,是否包含某个元素,清除,转换为数组等方法,对实现者的限制比较小,仅仅限制了继承该抽象类的实现必须能使用迭代器遍历整个集合即可。
需要注意的方法只有toArray()以及重载形式和它的辅助方法:
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
r[i] = it.next();
}
return it.hasNext() ? finishToArray(r, it) : r;
}
在public Object[] toArray()中,声明Object数组之后,获取迭代器之前数组的实质大小可能发生变化,至于变大还是变小是未知的,所以在使用for循环遍历元素时,有这一句:
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
返回时是这样的:
return it.hasNext() ? finishToArray(r, it) : r;
这里分析一下:当集合变小,数组大小却大于预期大小,在for循环遍历时, if条件会成立,这时会直接返回Array.copyOf(r,i)也就是返回截断后长度为i的数组。
当集合变大,数组的大小也就小于预期大小,for循环结束后会返回finishToArray(r, it)
copyOf方法意义明了,实现清晰,且最终是由本地方法实现的,所以不予探究。
finishToArray方法:
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
if (i == cap) {
int newCap = cap + (cap >> 1) + 1;
// overflow-conscious code
if (newCap - MAX_ARRAY_SIZE > 0)
newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r, newCap);
}
r[i++] = (T)it.next();
}
// trim if overallocated
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
这里需要注意的是,每次循环都会增容原大小的一半加一个单位,所以内部的if有个溢出判断,如果增容后的大小大于数组最大大小则使用hugeCapacity函数获取一个合适的值:
- 如果增容后的大小溢出为负数则抛出异常
- 否则根据值的大小返回Integer.MAX_VALUE 或 MAX_ARRAY_SIZE
当然,如果返回的值是Integer.MAX_VALUE那么显而易见,在创建数组时会发
生java.lang.OutOfMemoryError异常,如果返回的是MAX_ARRAY_SIZE则会创建最大容量数组。
最后的返回阶段:
return (i == r.length) ? r : Arrays.copyOf(r, i);
由于循环时每次迭代器前进一个单位,i就自增一个单位,所以i的值一直保持着数组的真实大小,而r.length是使用newCap扩容增长的,所以最后一步是去除过度分配的空间。
在这里也看到了,由于性能关系,每次扩容都得调用copyOf,而调用该函数的开销是很大的,所以每次扩容2/n+1个单位,这样一来,在少数情况下可能导致扩容至溢出的情况,即大于MAX_ARRAY_SIZE的情况,也就是说如果当
MAX_ARRAY_SIZE > cap > (MAX_ARRAY_SIZE-1)/1.5
cap扩容会直接溢出,虽然这种情况很少,但这是一个值得注意的点,如果能确估数组的容量请手动分配,因为自动分配容量可能导致如上情况,造成不必要的溢出。
public <T> T[] toArray(T[] a) {
// Estimate size of array; be prepared to see more or fewer elements
int size = size();
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) { // fewer elements than expected
if (a == r) {
r[i] = null; // null-terminate
} else if (a.length < i) {
return Arrays.copyOf(r, i);
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
r[i] = (T)it.next();
}
// more elements than expected
return it.hasNext() ? finishToArray(r, it) : r;
}
在toArray的重载方法中,该方法可以根据传入参数来返回指定的类型数组。
可以看出,如果传入类型数组的长度不够,该方法会使r引用利用反射创建的size()长度的类型数组,否则使r引用参数t。
之后大部分思想都和toArray()方法类似,不同的地方是在toArray(T[] a)方法中存在两个类型数组的引用a和r,在创建集合迭代器之后,集合的大小可能发生变化,所以传入参数的类型数组也需要考虑使用。特别是在集合变小的情况下,如果入参数组的容量能容纳该集合,则最后将集合数据存入入参数组,并且多余的容量以null为分界线,最后返回入参数组,否则将数据存入反射创建的数组并返回该数组。
后面的contains,remove方法都是一致的,对null和!null两种情况进行分别处理。
addAll,clear,containsAll,removeAll,retainAll都是使用迭代器直接进行迭代操作。
二、AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E>
implements List<E> {}
由于该类内容比较多,所以打算就不摆上来了。该类新增的内容主要是迭代器和视图容器两个方面。
首先是迭代器方面,它们是作为内部类的形式出现的:
private class Itr implements Iterator<E> {}
private class ListItr extends Itr implements ListIterator<E>{}
Itr实现了Iterator接口,它使用cursor变量来标志迭代器的位置,使用lastRet变量保存上次迭代器所经过的位置(初始值以及删除后的值为-1)。同时,它还使用expectedModCount变量来实现了快速失败机制:
使用集合自身的方法修改集合的大小会修改modCount值,使用迭代器的方法修改集合大小会同步modCount和expectedModCount的一致性,迭代器操作集合时,迭代器验证expectedModCount值若和modCount不一致则抛出异常
ListItr对比Itr多实现了向前遍历的功能,以及可以获取下一个元素(cursor)和上一个元素(cursor-1)的索引和增加元素/更新元素 等具体功能。
视图方面,由以下提供:
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));}
它会根据当前实例是否实现RandomAccess接口来决定使用哪种视图。
这里需要注意的是,子视图实际上就是对比原来的列表多维护了几个范围变量而已,创建某列表的子视图后如果对原列表进行结构性修改,子视图将无法使用。
当然,子视图的用处还是挺多的,比如
list.sublist(a,b).clear()可以删除原视图的某个范围
list.sublist(a,b).clone()可以获取子视图的克隆等
三、AbstractSequentialList
AbstractSequentialList是顺序表的抽象基类,所以抽象程度对比AbstractList进一步降低,决定了元素的存储形式。该抽象类比较简单,因为继承于AbstractList,所以仅需要改动一小部分即可:
public abstract class AbstractSequentialList<E> extends AbstractList<E>{}
该抽象类的方法都是基于迭代器实现的,所以继承该类的子类最小仅需要实现迭代器部分和AbstractList中的抽象部分即可。
四、AbstractMap
public abstract class AbstractMap<K,V> implements Map<K,V>{}
AbstractMap类中常规方法和AbstractList差不多,只不过存储的元素变成了Entry<K,V>类型,需要注意的点是:
1.AbstractMap 允许空键值
2. AbstractMap 也没有给出增删元素的方法
3. add方法是抛出异常,remove方法是调用迭代器的remove方法
4. keySet()和values()方法分别返回键值对的键视图集合和值视图集合。并且将视图保存在Map实例的属性keySet和values中,因为视图是无状态的,所以没有理由创建多个视图。所以自定义的获取键/值视图的方法应该更新这两个字段。
五、AbstractSet
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
protected AbstractSet() {}
public boolean equals(Object o)
public int hashCode()
public boolean removeAll(Collection<?> c)
}
该类比较简单,大部分方法都继承于AbstractCollection,需要注意点是:
- removeAll(Collection<?> c)方法优化为迭代两个集合中比较小的集合进行遍历删除。比较不解的是为什么AbstractSet优化成这样,而AbstractCollection却还是老样子…
- equals()方法将类型紧缩为Set
- 实现了Set基本的hashCode()方法
六、小结
Java集合框架中,抽象类的定义是十分谨慎的,它们只保证了最小的实现,提取了抽象类子类的最大共性部分,比如抽象类都没有决定底层数据的存储方式,所以,add,remove,set等直接操作底层容器的方法在所有抽象类中都没有具体实现,或者只有抛出异常的实现。但经过一层迭代器的封装,就可以在没有具体定义的情况下实现一致的操作接口。所以,在之后的接口中,只要继承了有迭代器的实现,就可以轻松的改变接口内容。
实际上,关于把操作容器底层存储的方法定义在类中还是迭代器中是非常暧昧的。
若把容器底层存储的操作都定义在该容器的迭代器中,该容器向外提供的接口就可以直接调用迭代器进行操作。比如AbstractSequentialList。这种实现一般都只存在于抽象类中,这样并没有太多好处,好处仅仅是继承该类的实现的最小代码量比较少而已。比如继承AbstractSequentialList的类,只需要实现迭代器和size()即可使用。
同理,如果将容器底层存储的操作直接定义在类中,迭代器关于底层存储的操作也可以调用类的方法,比如AbstractList。
关于hashcode()方法,在上层抽象类(AbstrcatCollection)中是没有实现的,在下层抽象程度低一点,决定了元素存储性质的抽象类中才给予了hashcode()方法的实现,比如在AbstractSet中:
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
该hashcode()方法是累加实现的。而在AbstractList中:
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
该hashcode()是以累加和乘积的形式实现的,在AbstractMap中:
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
该hashcode()是由键累加而成的。
实际上hashcode的定义并没有太多规定,为了使hashcode分布更均匀,一般都是将类中的关键/数据字段混迹其中,或加或乘或位运算,但需要保证的一点是,使用equals方法返回true的两对象,hashcode必须相等。只有这样才能正确的使用hashcode。