简介
ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。
存储结构
transient Object
[] elementData
;
Object类型数组。
数据域
private static final long serialVersionUID
= 8683452581122892189L
;
private static final int DEFAULT_CAPACITY
= 10;
private static final Object
[] EMPTY_ELEMENTDATA
= {};
private static final Object
[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
= {};
transient Object
[] elementData
;
private int size
;
private static final int MAX_ARRAY_SIZE
= Integer
.MAX_VALUE
- 8;
protected transient int modCount
= 0;
ArrayList的无参构造函数。初始化的时候并没有真正的创建10个空间,这是惰性初始模式对象。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的区别仅仅是为了区别用户带参为0的构造和默认构造的惰性初始模式对象。modCount用来记录ArrayList结构发生变化的次数。用于Fail-Fast机制
构造函数
public ArrayList() {
this.elementData
= DEFAULTCAPACITY_EMPTY_ELEMENTDATA
;
}
public ArrayList(int initialCapacity
) {
if (initialCapacity
> 0) {
this.elementData
= new Object[initialCapacity
];
} else if (initialCapacity
== 0) {
this.elementData
= EMPTY_ELEMENTDATA
;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity
);
}
}
public ArrayList(Collection
<? extends E> c
) {
elementData
= c
.toArray();
if ((size
= elementData
.length
) != 0) {
if (elementData
.getClass() != Object
[].class)
elementData
= Arrays
.copyOf(elementData
, size
, Object
[].class);
} else {
this.elementData
= EMPTY_ELEMENTDATA
;
}
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的区别仅仅是为了区别用户带参为0的构造和默认构造的惰性初始模式对象。注意深拷贝和浅拷贝。带参为0的构造会惰性初始化,不为0的构造则不会惰性初始化。
add()源码解析
public boolean add(E e
) {
ensureCapacityInternal(size
+ 1);
elementData
[size
++] = e
;
return true;
}
private void ensureCapacityInternal(int minCapacity
) {
ensureExplicitCapacity(calculateCapacity(elementData
, minCapacity
));
}
private static int calculateCapacity(Object
[] elementData
, int minCapacity
) {
if (elementData
== DEFAULTCAPACITY_EMPTY_ELEMENTDATA
) {
return Math
.max(DEFAULT_CAPACITY
, minCapacity
);
}
return minCapacity
;
}
private void ensureExplicitCapacity(int minCapacity
) {
modCount
++;
if (minCapacity
- elementData
.length
> 0)
grow(minCapacity
);
}
private void grow(int minCapacity
) {
int oldCapacity
= elementData
.length
;
int newCapacity
= oldCapacity
+ (oldCapacity
>> 1);
if (newCapacity
- minCapacity
< 0)
newCapacity
= minCapacity
;
if (newCapacity
- MAX_ARRAY_SIZE
> 0)
newCapacity
= hugeCapacity(minCapacity
);
elementData
= Arrays
.copyOf(elementData
, newCapacity
);
}
扩容操作需要调用Arrays.copyOf()把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,减少扩容操作的次数。
add(int index, E element)源码分析
public static native void arraycopy(Object src
,
int srcPos
,
Object dest
,
int destPos
,
int length
);
public void add(int index
, E element
) {
rangeCheckForAdd(index
);
ensureCapacityInternal(size
+ 1);
System
.arraycopy(elementData
, index
, elementData
, index
+ 1,
size
- index
);
elementData
[index
] = element
;
size
++;
}
需要调用System.arraycopy()将包括index在内后面的元素都复制到index + 1位置上,该操作的时间复杂度为O(N),可以看出ArrayList数组头增加元素的代价是非常高的。
remove(int index)源码分析
public E
remove(int index
) {
rangeCheck(index
);
modCount
++;
E oldValue
= elementData(index
);
int numMoved
= size
- index
- 1;
if (numMoved
> 0)
System
.arraycopy(elementData
, index
+1, elementData
, index
,
numMoved
);
elementData
[--size
] = null
;
return oldValue
;
}
需要调用System.arraycopy()将包括index + 1在内后面的元素都复制到index位置上,该操作的时间复杂度为O(N),可以看出ArrayList数组头增加元素的代价是非常高的。
Fail-Fast机制
fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。
结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组大小,仅仅只是设置元素的值不算结构发生变化。在进行序列化或者迭代操作时,需要比较操作前后modCount是否改变,如果改变了需要跑出ConcurrentModificationException
private class Itr implements Iterator<E> {
int cursor
;
int lastRet
= -1;
int expectedModCount
= modCount
;
public boolean hasNext() {
return cursor
!= size
;
}
public E
next() {
checkForComodification();
}
public void remove() {
if (this.lastRet
< 0)
throw new IllegalStateException();
checkForComodification();
}
final void checkForComodification() {
if (ArrayList
.this.modCount
== this.expectedModCount
)
return;
throw new ConcurrentModificationException();
}
}
一个单线程环境下的fail-fast的例子
public static void main(String
[] args
) {
List
<String> list
= new ArrayList<>();
for (int i
= 0 ; i
< 10 ; i
++ ) {
list
.add(i
+ "");
}
Iterator
<String> iterator
= list
.iterator();
int i
= 0 ;
while(iterator
.hasNext()) {
if (i
== 3) {
list
.remove(3);
}
System
.out
.println(iterator
.next());
i
++;
}
}
序列化
ArrayList 实现了 java.io.Serializable 接口,但是自己定义了序列化和反序列化。因为ArrayList基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没有必要全部进行序列化。因此 elementData 数组使用 transient 修饰,可以防止被自动序列化。
private void writeObject(java
.io
.ObjectOutputStream s
)
throws java
.io
.IOException
{
int expectedModCount
= modCount
;
s
.defaultWriteObject();
s
.writeInt(size
);
for (int i
=0; i
<size
; i
++) {
s
.writeObject(elementData
[i
]);
}
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();
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中为什么size要序列化两次?
在代码中s.defaultWriteObject();中size应该也被序列化了,为什么下边还要再单独序列化一次呢? 这样写是出于兼容性考虑。 旧版本的JDK中,ArrayList的实现有所不同,会对length字段进行序列化。 而新版的JDK中,对优化了ArrayList的实现,不再序列化length字段。 这个时候,如果去掉s.writeInt(size),那么新版本JDK序列化的对象,在旧版本中就无法正确读取, 因为缺少了length字段。 因此这种写法看起来多此一举,实际上却保证了兼容性。
小结
ArrayList基于数组方式实现,无容量的限制(会扩容)添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量可以使用trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间。线程不安全add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))remove(Object o)需要遍历数组remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高contains(E)需要遍历数组