一、ArrayList的继承类图
- Arraylist实现
List
,提供了基础的添加、删除、遍历等操作。 - ArrayList实现
RandomAccess
,提供随机访问的能力。 - ArrayList实现
Cloneable
,可以被克隆。 - ArrayList实现
Serializable
,可以被序列化
二、ArrayList的成员变量
//默认的初始化容量
private static final int DEFAULT_CAPACITY = 10;
//空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//Object数组
transient Object[] elementData;
//数组的大小
private int size;
elementData
是一个Object类型的数组,我们就可以知道ArrayList的底层数据结构其实是数组来进行实现的,DEFAULT_CAPACITY
我们可以知道默认的初始化容量为10elementData
是用transient
来进行修饰,代表这个数组不会被序列化,但是ArrayList是支持序列化的,这个不是自相矛盾吗,我们了解一下基本的序列化原理。
- 对于 Java 对象来说,如果使用 JDK 的序列化实现。对象只需要实现
java.io.Serializable
接口。 - 可以使用
ObjectOutputStream()
和ObjectInputStream()
对对象进行手动序列化和反序列化。 - 序列化的时候会调用
writeObject()
方法,把对象转换为字节流。 - 反序列化的时候会调用
readObject()
方法,把字节流转换为对象。
Java 在反序列化的时候会校验字节流中的serialVersionUID
与对象的serialVersionUID
时候一致。如果不一致就会抛出InvalidClassException
异常。官方强烈推荐为序列化的对象指定一个固定的 serialVersionUID。否则虚拟机会根据类的相关信息通过一个摘要算法生成,所以当我们改变类的参数的时候虚拟机生成的 serialVersionUID 是会变化的。
-transient
关键字修饰的变量 不会 被序列化为字节流
在上面我们我们虽然知道了elementData
是被transient
进行修饰,但是我们也了解了对象的序列化和反序列化是通过调用方法 writeObject()
和 readObject()
完成,其实在ArrayList中是进行了自定义序列化,我们来看下ArrayList的这两个方法。
//序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// 获取modCount,防止在写入的期间进行修改
int expectedModCount = modCount;
//调用ObjectOutputStream的方法将当前类没有修改transient和不是
//static的字段写入流中,因为我们的elementData修饰了,所以这里不
//会写入
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
//这里写出数组的大小
s.writeInt(size);
//遍历整个数组,然后将数组中有值的元素写出去,因为一般数组扩容后会有很多的下标位置没有数据,这样我们在进行扩容的时候只把下标元素有数据的写入到流中
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
//这里判断modCount是否不等于expectedModCount,如果不等于,说明在操作期间,有其他线程进行修改了,那么直接抛出异常即可。
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
//反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// 读取数组
s.defaultReadObject();
// 读取数组的容量
s.readInt(); // ignored
//判断如果数组是有元素则进行反序列化数组的元素
if (size > 0) {
//计算容量
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
//检查是否需要进行扩容
ensureCapacityInternal(size);
Object[] a = elementData;
// 反序列化出数组的元素
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
在上面我们知道其实ArrayList是把性能用到了极致,在进行序列化的时候只对数组中元素有值的进行序列化和反序列化,减少了进行序列化的开销。
- 在成员变量中还有两个空数组,
EMPTY_ELEMENTDATA
是当用户在构造函数中传入的容量为0的时候会将elementData赋值为EMPTY_ELEMENTDATA
,当我们使用无参构造函数的时候会将elementData赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,代表使用默认的大小。
三、构造函数
- ArrayList(int initialCapacity) 构造函数
public ArrayList(int initialCapacity) {
//校验initialCapacity,小于0抛出异常,大于0就初始化数组
//等于0就赋值为空数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
在构造方法中首先对initialCapacity进行校验
- 小于0抛出异常
- 等于0初始化一个空数组
EMPTY_ELEMENTDATA
- 大于0,初始化一个
initialCapacity
大小的Object
数组
- ArrayList()构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- 在无参构造函数中会将elementData赋值为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
代表使用默认的容量。 - 这里我们发现只是这里没有进行初始化,这是懒加载的特性,将初始化放到了第一次添加的时候,在需要的时候才进行加载。
- ArrayList(Collection<? extends E> c)构造函数
public ArrayList(Collection<? extends E> c) {
//将集合转成Array数组
elementData = c.toArray();
//判断这个Array数组有没有值
if ((size = elementData.length) != 0) {
//c.toArray()可能错误的不返回Object数组,那么将其转成Object数组。
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// size为0则替换成EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 首先将集合转成Array数组,然后判断判断size是否为0,如果为0则替换成
EMPTY_ELEMENTDATA
,不为0,校验下数组是否是Object类型,如果不是则转成Object
类型。 - 这里为什么有可能返回的不是Object类型的,是因为多态的原因,Object是所有类的父类,具有向下兼容的特性,示例如下:
class ArrayTest {
public static void main(String[] args) {
Father[] fathers = new Son[]{};
//打印的结果:class [Lcom.ls.test.Son;
System.out.println(fathers.getClass());
//打印的结果:class [Ljava.lang.String;
List<String> strList = new MyList();
System.out.println(strList.toArray().getClass());
}
}
class Father{}
class Son extends Father{}
class MyList extends ArrayList<String>{
@Override
public Object[] toArray() {
return new String[]{"1","2","3"};
}
}
四、常用方法解析:
- add(E e)
public boolean add(E e) {
//确保数组容量可以进行添加元素
ensureCapacityInternal(size + 1); // Increments modCount!!
//默认放在最后
elementData[size++] = e;
return true;
}
ensureCapacityInternal
方法
调用
calculateCapacity()
计算出一个容量,然后调用ensureExplicitCapacity()
来确保当前数组的大小能够满足这个容量。
private void ensureCapacityInternal(int minCapacity) {
//这里会调用calculateCapacity(elementData, minCapacity)计算容量,minCapacity=size+1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
calculateCapacity
方法
在这里判断是否是
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,我们在无参构造器中会将elementData
复制为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,如果是第一次添加的话DEFAULT_CAPACITY
是10,size+1=1,那么Math.max就会返回
DEFAULT_CAPACITY
//在这里判断是否是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,我们在无参构造器中会将elementData复制为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//返回DEFAULT_CAPACITY和minCapacity的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
ensureExplicitCapacity
方法
在这里让
modCount
++,然后判断当前数组能否满足计算出来的最小容量,不能满足调用grow()
方法进行扩容
//因为是添加了一个元素,所以在这里让modCount++
modCount++;
//判断当前数组的长度能否满足计算出来的最小容量,如果不能满足则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
grow
方法
默认是将原数组的大小扩容
1.5
倍,如果扩容1.5倍后的数组还小于minCapacity则将新数组的大小设置为minCapacity
,后面在判断newCapacity
如果是大于MAX_ARRAY_SIZE
,则调用hugeCapacity()
重新设置newCapacity,最后调用Arrays.copyOf()
将进行扩容拷贝。
Integer.MAX_VALUE - 8
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//在这里发现扩容后的容量是之前的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断扩容两倍是否满足minCapacity,如果不满足,就让新数组的容量等于minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//判断扩容后的容量是否大于MAX_ARRAY_SIZE,如果超出了MAX_ARRAY_SIZE限制,则调用hugeCapacity()
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
hugeCapacity
方法
在这里首先判断
minCapacity
是否超过了int的最大值,如果发生溢出则直接抛出异常,如果没有溢出则判断minCapacity
如果大于MAX_ARRAY_SIZE
则返回int
的最大值,否则返回MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
//在这里如果minCapacity小于0则说明已经超过了int的最大值了,发生溢出了,则抛出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果没有溢出,然后则根据大小来判断返回Integer.MAX_VALUE还是MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
add(int index, E element)
方法
首先检查
index
的位置是否合法,然后校验下当前数组的容量是否满足添加一个元素,然后调用System.arraycopy
将Index
及其之后的元素全都向后挪动一个位置,数组中下标为index
的位置已经空出来了,直接赋值为element
即可,然后size
++。
public void add(int index, E element) {
//检查index下标的位置是否合法
rangeCheckForAdd(index);
//在这里和上面add一样,会检查容量是否满足
ensureCapacityInternal(size + 1); // Increments modCount!!
//调用System.arraycopy方法,将当前下标及其后面的元素都向后拷贝一个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//下表为index的位置已经空出来了,给index下标的元素赋值
elementData[index] = element;
//size进行++
size++;
}
rangeCheckForAdd
方法
校验
index
的位置是否合法,判断是否<0或者是否>size,如果满足就抛出数组下标越界异常,这也是最常碰到的异常😄
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
在这里有一个问题,为什么在调用grow()
方法扩容的时候是调用Arrays.copyOf()
来进行扩容,而在这里是调用System.arraycopy()
来进行扩容的,他们俩有什么区别?
我们追踪进去发现System.arraycopy()
是native
方法
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
我们再看下Arrays.copyof()
方法
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
调用了重载方法,我们继续跟进去,发现其实底层也是调用了System.arraycopy()
方法进行的拷贝,只是在上面包装了一层,做一些额外的操作,比如copy
数组的创建,范型的转换等。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
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;
}
- 我们看下
System.arraycopy()
的底层实现,我们知道native
方法使用c++实现的,java
通过JNI底层调用c++
的方法,是通过Java的System类有个静态代码块,他会执行registerNatives本地方法
private static native void registerNatives();
static {
registerNatives();
}
这个会调用到System.c
中的Java_java_lang_System_registerNatives
方法,在这个方法中我们看到他会把三个本地方法绑定到JVM对应的方法中,arraycopy
对应的是JVM_ArrayCopy
方法
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
4. 我们看下对应的JVM_ArrayCopy()函数。
JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
jobject dst, jint dst_pos, jint length))
JVMWrapper("JVM_ArrayCopy");
//检查原数组和目标是否为空,为空抛出空指针异常
if (src == NULL || dst == NULL) {
THROW(vmSymbols::java_lang_NullPointerException());
}
//在这里将原数组对象和目标数组对象转成arrayOop,c++的数组对象
arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
//然后断言判断他们是否为对象
assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
//在这里真正调用复制的地方,调用copy_array方法
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
在 s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
这里根据不同的klass()有不同的处理,是根据Java的不同类型来对应返回不同的klass,数组里面的元素类型可以分为基本数据类型和对象类型,对应到JVM中就是TypeArrayKlass和ObjArrayKlass。
- TypeArrayKlass
在
TypeArrayKlass::copy_array()
方法中首先做一些参数的校验,然后将起始结束为转成char*
提高赋值效率,然后计算出来数组元素类型长度的log值,可以通过位移快速计算位置,还会记录第一个元素的偏移,后面就调用到Copy::conjoint_memory_atomic
,根据不同的基本类型调用各自的函数,因为已经有起始和结束指针,可以根据不同的类型进行快速的内存操作。
void TypeArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d, int dst_pos, int length, TRAPS) {
//检查是否是数组
assert(s->is_typeArray(), "must be type array");
//检查数组中元素的类型
if (!d->is_typeArray() || element_type() != TypeArrayKlass::cast(d->klass())->element_type()) {
THROW(vmSymbols::java_lang_ArrayStoreException());
}
//检查下标参数是否是非负的
if (src_pos < 0 || dst_pos < 0 || length < 0) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
//检查下标参数是否越界
if ( (((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length())
|| (((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length()) ) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
// 判断如果长度为0则不需要进行拷贝
if (length == 0)
return;
// This is an attempt to make the copy_array fast.
//这是为了让array_copy更快,因为这里设置到内存指针的移动,为了提高复制操作的效率将起始位置和结束为止转成char*,log2_element_size就是计算数组元素类型长度的log值,后面通过位移操作能快速计算位置。而array_header_in_bytes计算第一个元素的偏移。
int l2es = log2_element_size();
int ihs = array_header_in_bytes() / wordSize;
char* src = (char*) ((oop*)s + ihs) + ((size_t)src_pos << l2es);
char* dst = (char*) ((oop*)d + ihs) + ((size_t)dst_pos << l2es);
//接着到Copy::conjoint_memory_atomic函数,这个函数的主要逻辑就是判断元素属于哪种基本类型,再调用各自的函数。因为已经有起始和结尾的指针,所以可以根据不同类型进行快速的内存操作。这里以整型类型为例,将调用Copy::conjoint_jints_atomic函数。
Copy::conjoint_memory_atomic(src, dst, (size_t)length << l2es);
}
conjoint_memory_atomic
函数中会根据不同的类型调用不同的方法,我们可以继续看conjoint_jints_atomic()
·
void Copy::conjoint_memory_atomic(void* from, void* to, size_t size) {
address src = (address) from;
address dst = (address) to;
uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;
if (bits % sizeof(jlong) == 0) {
Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong));
} else if (bits % sizeof(jint) == 0) {
Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint));
} else if (bits % sizeof(jshort) == 0) {
Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort));
} else {
// Not aligned, so no need to be atomic.
Copy::conjoint_jbytes((void*) src, (void*) dst, size);
}
在conjoint_jints_atomic
中会调用pd_conjoint_jints_atomic
函数,这个函数在不同的操作系统下有不同的实现。
// jints, conjoint, atomic on each jint
static void conjoint_jints_atomic(jint* from, jint* to, size_t count) {
assert_params_ok(from, to, LogBytesPerInt);
pd_conjoint_jints_atomic(from, to, count);
}
我们看下在windows系统下的实现,主要逻辑是分成两种情况复制:向前复制和向后复制。并且是通过指针遍历数组来赋值,这里进行的是值拷贝,也就是深拷贝。对于long、short、byte等类型也是做类似的处理,但在某些操作系统的某些cpu架构上会使用汇编来实现。
static void pd_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
if (from > to) {
//向前复制
while (count-- > 0) {
// Copy forwards
*to++ = *from++;
}
} else {
//向后复制
from += count - 1;
to += count - 1;
while (count-- > 0) {
// Copy backwards
*to-- = *from--;
}
}
}
- ObjArrayKlass
前面和基本类型是一致的,做一些参数的校验,在下面UseCompressedOops标识表示对 JVM 中Java对象指针压缩,主要表示用32位还是64位作为对象指针,未压缩的情况下,会调用do_copy函数。
void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
int dst_pos, int length, TRAPS) {
...........
if (UseCompressedOops) {
narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
do_copy<narrowOop>(s, src, d, dst, length, CHECK);
} else {
oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
do_copy<oop> (s, src, d, dst, length, CHECK);
}
}
这里会进行s==d的判断是因为源数组和目标数组可能是相等的,而如果不相等的情况下则要判断源数组元素类型是否和目标数组元素类型一样,如果一样的话处理也做类似处理,另外这里还添加了是否为子类的判断。以上两种情况核心赋值算法都是
Copy::conjoint_oops_atomic
。
template <class T> void ObjArrayKlass::do_copy(arrayOop s, T* src,
arrayOop d, T* dst, int length, TRAPS) {
BarrierSet* bs = Universe::heap()->barrier_set();
if (s == d) {
bs->write_ref_array_pre(dst, length);
Copy::conjoint_oops_atomic(src, dst, length);
} else {
Klass* bound = ObjArrayKlass::cast(d->klass())->element_klass();
Klass* stype = ObjArrayKlass::cast(s->klass())->element_klass();
if (stype == bound || stype->is_subtype_of(bound)) {
bs->write_ref_array_pre(dst, length);
Copy::conjoint_oops_atomic(src, dst, length);
} else {
...
}
}
bs->write_ref_array((HeapWord*)dst, length);
}
该函数也跟操作系统和cpu架构相关,这里看windows_x86的实现,很简单也是直接通过指针遍历赋值,oop是JVM层的对象类,而且该类也没有重写operator=操作符的,默认情况下是拷贝地址的,所以它们还是指向同一块内存,这反应到 Java 层也是这样的。即所谓的“浅拷贝”。
static void conjoint_oops_atomic(oop* from, oop* to, size_t count) {
pd_conjoint_oops_atomic(from, to, count);
}
static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
if (from > to) {
while (count-- > 0) {
*to++ = *from++;
}
} else {
from += count - 1;
to += count - 1;
while (count-- > 0) {
// Copy backwards
*to-- = *from--;
}
}
}
System.arraycopy为 JVM 内部固有方法,它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。数组越大体现地越明显。
remove(int index)
方法
首先也是检查下标位置是否合法,然后让
modCount++,
计算出来移动的位置,如果numMoved
是大于0的说明需要移动元素,如果不是大于0说明删除的就是最后一个元素,然后调用System.arraycopy
移动完成后将之前数组的最后元素释放掉,可以在下次gc
的时候进行回收。
public E remove(int index) {
//检查index是否合法
rangeCheck(index);
//对modCount++
modCount++;
//获取删除下标位置的值
E oldValue = elementData(index);
//计算出来移动的位置
int numMoved = size - index - 1;
//如果大于0才需要进行移动
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//这里让--size的位置置为空,可以让gc进行回收。
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
remove(Object o)
方法
这里清除可以传null,如果传null,他会清理数组中为null的元素,清理的时候都是会调用
fastRemove
方法,其实这个方法会将元素进行移动,当输入null,相当于一个整理的过程。
public boolean remove(Object o) {
//判断对象是否为空,
if (o == null) {
//遍历数组,清除数组中下标为空元素
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//遍历数组,清除下标位置等于o的元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
fastRemove(int index)
方法
这里ModCount++,然后将后面的元素位置往前挪动一个下标,然后让之前的size下标的元素置为空。
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
五、快速失败与安全失败
1、快速失败
-
我们在上面的常用方法中都能看到在做添加或者是删除的时候都会对一个变量
modCount
进行++,那这个变量到底是做什么的?其中ArrayList就是通过这个变量来实现快速失败,我们一起来看一下。 -
从之前的类图我们可以看到
ArrayList
是实现Iterable
接口的,那么就表示可以使用迭代器来进行遍历数组中的元素,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
,这个就是快速失败,快速失败的实现就是依赖于modCount
这个变量。 -
我们看下ArrayList.iterator()方法会返回一个Itr,这个是ArrayList的一个内部类我们继续看一下Itr。
public Iterator<E> iterator() {
return new Itr();
}
- Itr的成员变量
这里我们会发现expectedModCount初始化的时候是等于modCount的
private class Itr implements Iterator<E> {
int cursor; // 要返回的下一个元素的索引
int lastRet = -1; // 返回的最后一个元素的索引,如果没有返回-1
int expectedModCount = modCount; //expectedModCount初始化的时候是等于modCount的。
- 我们在使用迭代器进行遍历的时候会通过
hasNext()
来判断有没有下一个元素,通过next()
来遍历下一个元素,我们来看下这两个方法。
我们发现在使用
next()
遍历中刚开始就调用checkForComodification()
方法来进行检查是否有并发操作,而checkForComodification()
就是判断当前的expectedModCount
是否等于modCount
,我们知道初始化迭代器的时候会把当前的expectedModCount
赋值为modCount
,而执行添加或者删除方法都会使modCount++
,如果他俩不相等说明在遍历的过程中,有另外的线程添加或者删除数组中的元素了。
后面处理也判断
cursor的是否>=size``,>=elementData.length
是为了防止同时有多个线程对这个迭代器进行遍历,导致数组下标越界。
hasNext()
方法
public boolean hasNext() {
//判断cursor是否不等于size,不等于说明还有下一个
return cursor != size;
}
next()
方法
public E next() {
//这个方法就是来检查是否有并发修改
checkForComodification();
int i = cursor;
//如果cursor>=size,说明后面已经没有元素了,直接抛出异常
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//如果在遍历的过程中i>=数组的大小,那么说明有其他线程在进行并发操作,那么抛出异常
if (i >= elementData.length)
throw new ConcurrentModificationException();
//这里先cursor先++,然后在返回。
cursor = i + 1;
return (E) elementData[lastRet = i];
}
checkForComodification()
方法
就是判断
modCount
是否等于expectedModCount
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
我们从上面可以知道
ArrayList
是一个线程不安全的集合
2、安全失败。
- 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
-
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发
Concurrent Modification Exception
。 -
缺点:基于拷贝内容的优点是避免了
Concurrent Modification Exception
,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
2.我们来看下CopyOnWriteArrayList
的实现
返回一个COWIterator,在getArray()也是直接返回当前的数组对象,和ArrayList是一样的,那么复制是在哪里复制的?我们继续看下COWIterator对象。
iterator()
方法
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
getArray()
方法
final Object[] getArray() {
return array;
}
COWIterator
类的实现
我们发现在
COWIterator
也没有做任何数组的拷贝,但是他把这个数组叫做snapshot,为什么呢?我们看下CopyOnWriteArrayList
的修改的方法,看在修改的时候有没有做处理。
static final class COWIterator<E> implements ListIterator<E> {
//快照数组
private final Object[] snapshot;
//遍历的游标
private int cursor;
//构造函数中也没有做拷贝
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
CopyOnWriteArrayList()
的add()
方法。
我们发现它原来是在写入的时候创建一个新数组然后替换掉原来的数组,那么我们在迭代器中引用的数组就不是当前数组了,也就是一个
snapShot
的数组了。
public boolean add(E e) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前数组
Object[] elements = getArray();
//获取当前数组的大小
int len = elements.length;
//在写入的时候复制一个新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//设置当前的array指向新数组
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
总结:虽然ArrayList我们很常用,但是其中内部原理中很多优化,实现比较好的地方是需要我们进行学习掌握并借鉴的,希望大家能够追求极致,全力以赴!
由于对hospot源码掌握不深,System.arraycopy()原理部分参考自 System.arraycopy为什么快