之前看到了removeAll和retainAll方法里面都调用了batchRemove方法,只不过传递的参数不同,于是决定好好的看一看这个方法。
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
乍一看觉得还是可以理解的,但是看到第8行的时候就一脸的懵逼了,”elementData[w++]=elementData[r]“名字都一样的,你让怎么知道你的意思?于是我尝试从两个int类型变量名字入手。变量r应该是read的意思,意思就是读取,这种解释也符合了为什么for循环里面不断变化的是r。变量w应该就是write的意思,写入的意思,这也解释了为什么当判断条件成立的时候,w才会发生变化。
有了这层理解就好了,既然r是读取,w是写入,那么”elementData[w++]=elementData[r]“前面的这个elementData是方法自己定义的那个变量,后面那个elementData是ArrayList自己定义的变量。也就是说,前面这个elementData在出了batchRemove方法后就没有用了,于是我将代码自己理解了一下:
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] BRelementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(BRelementData[r]) == complement)
elementData[w++] = BRelementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(BRelementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
这样一来就很容易理解了,根据”c.contains(elementData[r]) == complement“这个的结果,BRelementData里面存储的就是想要的结果了。而这段for循环却被try包裹着,后面还跟上了一个finally结构。finally的作用是,try里面的代码执行完毕后必定执行finally里面的代码。而这样做给的理由在注释中也说明了,是为了保持和AbstractCollection的兼容性。这么说也不是很懂它的意思,还是看里面的代码比较实在一点。
if (r != size) {
System.arraycopy(BRelementData, r,
elementData, w,
size - r);
w += size - r;
}
首先,是判断r是否会等于size。如果说前面的代码不停止的话,那么最后r会将ArrayList里面的元素全部遍历完毕,最后r的值会等于size的值,并且跳出前面那个for循环。既然前面那个for循环执行完毕之后,r一定会等于size的,那为什么这里就要进行这样的判断呢?也就是说,这是在避免某种情况,避免什么样的情况,可以从代码中看得出来。
我们先让它能执行到if里面的语句,假设此时ArrayList里面的size是10,而我们的r=3,看看会发生什么。同样是熟悉的System.arraycopy方法,先将batchRemove方法自己的BRelementData数组10个元素从r=3开始进行复制,然后添加进ArrayList自己的elementData数组中,并且从w开始的位置加入,加入的长度是size-r也就是7个长度。
从r=3开始复制,前面的不复制是因为通过for循环已经遍历过了,所以复制的是那些没有遍历过的数。而后续添加的位置是从w开始,也就是说,没有遍历过的BRelementData的数据被添加到了经历过”if (c.contains(elementData[r]) == complement)“判断过后的数的后面。随后w的长度也增加了size-r个数。
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
前面的第一个if看完了,现在来看看关于w的if条件。我们先看看if里面做了什么样的事情,一个for循环,从w位置开始,一直遍历到size最后的位置,将w开始的元素以及之后的元素都设置成为null,然后重新设置size的长度为w,将后面所有的null都交给GC垃圾回收机制去处理。随后更改modified的值,表示ArrayList里面的数据确实发生了改动。
此时我们就知道了,finally里面的代码块是暂存作用,为什么只有(r!=size)的时候才去执行里面的代码块?明明try代码块里面的r最后肯定会是r=size才会跳出for循环的。那么为什么后面也要设置一个(w!=size)的判定呢?我们假设前面的直接走完了,并没有走(r!=size)里面的代码,此时有两种情况,刚好w=size,没有执行(w!=size)里面的代码,此时modified=false,我们就知道,ArrayList里面的数据并没有发生改动。另外一种情况,执行了(w!=size)里面的代码,最后size=w。
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
writeObject方法,传递的参数是一个ObjectOutputStream对象,这是一个写入流,在注释中也对这个方法进行了解释,将ArrayList的状态保存进流里面。此时,我们多次看到过的modCount就起到作用了,先定义了一个expectedModCount用来保存它。然后执行默认的序列化操作,然后将大小写入进去,然后通过for循环把所有元素按照正确的顺序写入进去。
每当ArrayList里面的内容被改动过的时候,modCount都会增加,if (modCount != expectedModCount)的判断是为了确保,当把ArrayList里面的数据写入流里面的时候是最新数据。如果在写入流的过程中,数据发生了变化,那么这个数据就不是最新的数据,那么就抛出错误ConcurrentModificationException()。
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
既然有写入,那么肯定就有读取。readObject方法和writeObject方法是在ArrayList里面的数据序列化的时候才会调用得到,既然是从流中读取出数据,那么就和writeObject差不多的操作,先将ArrayList里面的数据清空成为定义好的EMPTY_ELEMENTDATA空数组。然后是默认的序列化读取操作,读取里面的int,然后判断里面的大小,随后扩容,然后遍历将元素都取出来。
虽然很纳闷这这个方法,但我的理解是,for循环中存在着遍历器,即使你看起来所有的a[i]=s.readObject(),但实际上,是按照顺序一一对应读取出来的,for循环里面应该有遍历器去呼应readObject的动作,达到进去的时候是什么顺序,读出来的就是什么顺序。
关于OutputStream和InputStream的记忆有一个小技巧。可以想象成寄快递和收快递,OutputStream就像是寄快递,你把需要传递的东西打包好,然后写各种信息就OK了,这就是写入流。InputStream就像是收快递,你收到一样东西,你肯定先看下是不是你的,读包裹上面的字,然后拆开包裹取出里面的东西,这就是读出流。一个写入数据和读取数据就这样记住了。