AbstractCollection提供了几个通用的Collection的具体实现方法,如果想实现简单Collection接口的话,你的实现类可以选择继承这个类,只需要重写或者是实现size()和iterator()方法,但是,如果你想实现一个支持增删集合内元素的集合的话,你还需要自己重写add()和remove().因为AbstractCollection不支持add()和remove(),如果直接调用,会抛出UnsupportOperationException.
首先,我们先来看定义
public abstract class AbstractCollection<E> implements Collection<E>
特点:1 抽象类 2 实现了Collection
看他的方法和属性。
默认构造方法,用于子类构造函数调用
protected AbstractCollection() {
}
两个抽象方法,自己留着实现
public abstract Iterator<E> iterator();
public abstract int size();
判断集合内有没有元素,
public boolean isEmpty() {
return size() == 0;
}
不过这个size()是由子类自己实现的,返回值不一定准确代表集合内到底有多少个元素。如果子类实现size()的时候直接返回个0,那么无论集合内有没有元素,有多少元素,isEmpty都会返回true。除非子类重写isEmpty()。
判断集合内是否包含某个元素
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}
代码逻辑很简单。首先先看参数o是不是null,如果是null的话,迭代遍历集合,如果某个元素为null,说明集合包含null。如果o不为null,则使用参数类的equals()方法与迭代器遍历出的每一个元素进行对比,匹配成功,则说明包含此元素
集合转Object数组
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;
}
看这个方法的时候,我需要说明一点,在集合转数组的过程中,如果并发量足够大,而且子类支持在遍历期间对集合进行增删操作的情况下,这个方法返回的数组内拥有的元素数量,和集合本身拥有的元素数量,可能会出现不一致。
来看具体的逻辑。首先初始化一个长度为size()的Object数组。然后根据数组的下标从0开始为数组赋值,同时使用迭代器遍历集合。我们先看if条件不成立的时候,会顺序的将集合内的元素赋值给数组,这点非常好理解。主要看if条件成立的时候,到底是个什么样的逻辑。
不过,为了加深理解,我们首先来考虑几种情况。
第一种:集合不支持在(迭代器)遍历期间并发的对集合做增删操作。
这种情况下,if条件永远不会成立。因为数组的长度就是集合的size(),迭代器每一次指向的元素位置和数组循环遍历的元素位置是一样的。而且,在return的时候又一次判断了集合内还有没有元素,当然,这种情况下是肯定没有的。直接返回Object数组r。
第二种:集合支持在(迭代器)遍历期间并发的对集合做增删操作。
我们模拟这样一种场景:线程A此时拿到集合a的对象,对其进行toArray()操作,此时集合a内的元素数量为10。那么toArray()方法内,此时数组r的长度就为10,然后开始for循环。在这个过程中由于某种原因,线程B在for循环没有执行完的时候(假设此时for循环内i为5)抢占了对a的使用权,并且删除了集合内的两个元素,此时集合内元素数量为8。删除完成后,线程B释放掉集合a对象,for循环继续执行。当i为8时,迭代器此时指向集合内的第8个元素,也是集合内最后一个元素,此时,if条件成立,返回值为一个长度为8的Object数组。数组内的元素为集合内没有被线程B删除的那8个元素。
然后,我们看第三种场景,那就是线程B不是删除元素,而是增加了2个元素,其余场景一样。我们继续推导结果。if条件是肯定不会成立的,return的时候会去调用finishToArray()。再看这个方法之前,我们先看一下现在的变量结构与值
数组r:长度为10,里面的元素为集合内的10个元素(不考虑排序的情况下,就是没有增加前的那10个元素)。
迭代器对象it。此时指向集合内的第10个元素。
集合:此时拥有12个元素。
我们此时看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);
}
注意,这个方法是私有的,说明,这个方法只能由AbstractCollection自己用,他儿子都不能用。事实上,用到这个方法的地方只有两处,这也提醒我们,尽量将重复代码进行抽取,写成方法。
代码逻辑:首先使用变量i记录数组r的长度。之后进入while循环,条件就是判断迭代器是否还有下一个元素。循环体内再次使用一个变量cap记录本次循环中数组r的长度。如果i和cap相等,说明数组长度不够了,需要扩容了,具体的扩容操作为重新分配一个新数组,新数组的长度为r的长度的1.5倍+1。(cap >> 1 是位运算,相当于cap/2)同时将数组r的所有元素拷贝到新数组中。
如果i和cap不相等,那么就将迭代器指向的下一个元素赋值给数组第i个位置,并且对i进行自增操作。
在扩容过程中涉及到一个成员变量和一个方法,先看变量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
这是数组长度所能允许的最大值。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
方法逻辑很简单,就不解释了。
对于返回值的处理也很有意思,同样再次判断i的值是否与r相等。如果相等,说明数组内的元素数量恰好就是数组的长度,所以直接返回r就可以。如果不相等,说明新分配的数组长度超出了元素的总数,需要去除掉多分配的数组空间。
拿我们刚才的例子来说,一开始的时候i为10,迭代器中还有两个元素(或者说迭代器指向了第10个元素,后面还有11,12)开始循环的时候,if条件成立,需要对数组进行扩容,扩容后的新数组长度为16。再将11、12号元素放入新数组后,发现数组r内有空余的位置,也就是i不等于r.length。对r进行去空操作,也就是复制一个长度为i的新数组当做返回值。
视线再次回到toArray()方法,最后一种场景,返回值为长度为12的一个Object数组。
这就是线程不安全的表现之一。
集合转泛型数组
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()差不多,也会出现集合并发增删元素的问题。
首先使用一个变量size记录进入此方法时集合内的元素数量,然后开始分配数组。分配数据的时候又使用了最好玩的三目表达式。首先看参数数组a的长度是不是能够容纳集合内的所有元素,如果不够,就需要新分配一个长度为集合内元素数量的新数组。
根据集合内元素的数量,参数数组a的长度,以及集合是否允许在迭代器遍历期间进行增删操作,我们一共模拟以下几种场景。
第一种:集合不允许在迭代器遍历期间进行增删,集合内中元素的数量大于等于参数数组a的长度。
我们设定集合内有10个元素,即size()为10。数组的长度为5。
那么在循环之前,r为一个长度为10的泛型类型数组。
那么在循环中,if条件永远不会成立,数组r中存储的元素全部为集合内的元素,程序的返回值也是r,即一个包含集合内所有元素的,长度为集合元素数量的泛型类型数组。
第二种:集合不允许在迭代器遍历期间进行增删,集合内元素的数量小于参数数组a的长度。
让然设定集合内有10个元素,即size()为10,数组的长度此时设定为15。
r则为一个长度为15的泛型类型数组。
循环照常进行,当循环变量为10时,if条件成立,进入分支判断后,显然,a不等于r(因为此时r中已经存储了10个来自集合的元素),且a的长度(15)大于i(10),所以,会执行System.arraycopy()方法。我们简单的来看一下这个方法
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
这个方法不是由java实现的(具体请查阅native关键字相关知识),作用就是将数组src从srcPos开始,复制length个元素到数组dest中,放入的位置是数组dest的第destPos开始。
回到我们的分析,那么此时数组r中的i(10)个元素就会顺序的被复制到a中,并且下标不变。然后需要判断a的长度是否大于i,如果大于i,则剩下的空间用null来填充。
还记得Collection接口中对toArray(T[] a)的说明吗?如果期望的数组长度大于集合内的元素数量,那么多出来的数组空间全部用null来填充。这个方法就是体现。
第三种:集合允许在迭代器遍历期间进行增删,集合内元素的数量大于参数数组a的长度。
假设集合内有10个元素,参数数组a的长度为5。数组r为一个长度为10的数组。
假设在for循环执行到i为7的时候,集合删除了三个元素。那么函数的返回值就是拷贝r的前7个元素,组成一个新数组。
第四种,如果集合增加了元素,那么将会执行finishToArray()方法,最终返回一个包含集合所有元素的数组(包括新增加的元素)。
增加元素的方法
public boolean add(E e) {
throw new UnsupportedOperationException();
}
由此可见,AbstractCollection是不支持add操作的。子类如果需要此操作,需要自己重写这个方法。addAll也一样。
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
移除单个元素
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
使用迭代器进行遍历。分为两种情况,一是参数o为null,遍历中查找就集合中是否有null,有的话就移除,返回true。二是参数不为null,遍历查找是否有此元素(使用equals判断),有的话移除,返回真。都没有的情况下,返回false。
判断一个集合是否是当前集合的子集
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
没啥技术含量,循环调用contains。。。。
移除与参数集合的交集
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
首先呢,参数c不能为null,否则会报NPE。然后遍历调用参数C的contains方法,为true时就在本集合内移除。
移除差集
和removeAll逻辑差不多
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
清空集合
public void clear() {
Iterator<E> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
toString
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}