在之前《Java中的Collection》文章中简单粗略的介绍了Java中Collection前世今生及常用的Collection,这篇文章我们就单独聊聊ArrayList—一个Java开发者开发过程中绕不开的数据结构。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
通过这个可以查看到ArrayList继承了AbstractList并实现List、RandomAccess、Cloneable,Serializable等接口;通过这些接口我们大概知道了ArrayList拥有的基本的特性:
1. 必须保持元素特定的顺序(是一个List)
2. 支持快速(通常是固定时间)随机访问(RandomAccess)
3. 支持对象clone,是浅复制还是深复制,看是如何实现clone()方法(Cloneable)
4. 支持对象序列化(Serializable)
他是List
既然他是List,那么他就应该拥有List的一切特性—add/remove/iterator。
我们先看看ArrayList的构造
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);
}
}
通过以上代码我们可以发现,当构造一个ArrayList对象时创建了一个initialCapacity大小的数组—一个elementData成员变量。通过这个构造方法我们不会得到太多的信息,我们只知道ArrayList创建了一个指定大小的数组。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
上面就是List特性之一的add方法,他返回的结果永远都是true,另外执行了ensureCapacityInternal方法接着将e对象塞入elementData数组中。我们知道数组的大小不可变的,但是ArrayList不断的add对象进来,迟早会把elementData数组撑爆的,所以按照这个园里来推算,ArrayList是一个可变的”数组”,那么ensureCapacityInternal方法中应该进行了两个操作:
1. 判断elementData当前的容量是否足够
2. 按照扩容后大小迁移elementData中的数据
那么他是如何进行这些操作呢?看看下面的grow方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
从这个方法可以看出elementData新容量永远比老容量要高出1.5倍,扩容后进行copy操作创建新elementData,在这过程中JVM将elementData中的元素进行复制迁移到新的elementData中。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
grow是一个扩容、复制迁移数组的方法,那么ensureExplicitCapacity方法就是条件—判断是否需要执行grow方法。从条件判断语句我们可以清楚的看出,只要elementData.length比当前要求的最小容量大就足以跳过grow了,这个条件告诉我们设置按业务所需设置elementData的初始大小是非常重要,他可以间接的减少grow方法执行次数—例如你有一个数据流以20的倍数下发,这时候可以使用以20*0.75作为elementData的初始大小(具体根据业务情况而定)。
下面我们看看他的remove方法。
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; // clear to let GC do its work
return oldValue;
}
首先对index进行范围界定,该方法(rangeCheck)会抛出IndexOutOfBoundsException异常,然后通过elementData方法取出index位置上的对象。
E elementData(int index) {
return (E) elementData[index];
}
这是简单不能再简单的取值方法了,所以remove前必须保证index有效范围,否则会抛出数组越界异常。
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. 重组elementData数组(将index对应的value移除)
2. 将重组后的elementData最后一个元素设置为null
例如elementData中有5个元素,remove的index是3,所以numMoved为1,接着代码看就是显示为:
System.arraycopy(elementData, 4, elementData, 3, 1);
意思就是elementData从第4个位置(含)开始移动到elementData第三个位置,有效位置为1。通过 这样的一个copy操作就把需要移除的index对应的elementData元素给移除了(被以前第4个位置的元素替代了),但是我们发现elementData大小没变,并且新的elementData数组第3个元素和第4个元素是一样的,故需要将多余的重复的预算置为null。如下
elementData[--size] = null; // clear to let GC do its work
ArrayList迭代器
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
上面就是ArrayList实现Iterator接口实现的next、forEachRemaining等特性。这篇文章我们也主要针对next方法进行探讨。
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
从上面next方法可以看出,诸多index的if判断语句并且这些判断语句都有抛出异常的可能,这对于应用或者系统来说这是致命的。因此在执行迭代器操作的过程中一定要保证elementData 数组的数据完整性(禁忌对elementData 数组进行增删改操作)[此处在应用的过程中出现的问题比较多]。那如何避免呢?
1. 在创建迭代器后,使用迭代器内部的增删改方法。
2. 使用Collections.synchronizedList 方法将该列表“包装”起来,达到同步效果。
3. 创建的ArrayList创建一个副本,clone出一个副本。
支持快速随机访问
我们可以打开RandomAccess这个接口发现,发现他是一个空接口,那么他有什么用呢?
答案是他肯定有用的,不然Java不会设计这么个接口,他一般给一些Collections工具提供了判断依据,例如Collections的shuffle方法。
public static void shuffle(List> list, Random rnd) {
int size = list.size();
if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
for (int i=size; i>1; i--)
swap(list, i-1, rnd.nextInt(i));
} else {
Object arr[] = list.toArray();
// Shuffle array
for (int i=size; i>1; i--)
swap(arr, i-1, rnd.nextInt(i));
// Dump array back into list
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
ListIterator it = list.listIterator();
for (int i=0; i
可以看出,当你实现了RandomAccess时,他会调用另外一套代码块而达到区分。同理copy方法也是同样的道理。
public static void copy(List super T> dest, List extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; ielse {
ListIterator super T> di=dest.listIterator();
ListIterator extends T> si=src.listIterator();
for (int i=0; i
另外我们可以发现ArrayList的兄弟类LinkedList没有实现RandomAccess,一个实现了一个没实现,在算法上使用肯定有区别的,具体我们在以后的文章细说。
支持对象clone
clone方法是Object的方法,我们都知道一切皆是对象,那么世界万物都是Object的子类,所以clone方法也就被传下来的了。那么仅仅重写clone()方法就可以达到复制效果么?
答案肯定是错误的。我们知道clone()是伴有CloneNotSupportedException异常的—Object的clone()方法是一个native方法,当你调用它的clone的方法,发现这个对象时不支持clone就会抛出异常,详细后面的文章细讲。
public Object clone() {
try {
ArrayList> v = (ArrayList>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
从上面代码可以看出,ArrayList的clone是一种浅拷贝,关于对象的拷贝方式(浅拷贝、深拷贝),在后面的文章细讲
对象序列化
这个话题比较大,可以单独用一篇文章讲解,这里我就简单说明下—看看ArrayList如何实现的。
一个对象序列化(serialize)肯定必然出现反序列化(deserialize),他们所对应的方法writeObject和readObject。
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; iif (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
上面是序列化方法,也就是Object to byte[]的过程,我们可以看出,他是先将size写入,然后依次转换数组中的各个Object。
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,然后通过size确定当前size的大小,然后再反序列化各个Object。并且谁先write ,就先read谁,write的什么样的类型,read的也就是什么样的类型。关于序列化,我们在后面的文章详细讲到。
从上面的代码中我们可以看出,在序列化的过程中,如果执行了elementData 数组的数据完整
性的操作,将会抛出异常,这和在迭代的过程中,不能进行操作是一致的。执行完序列化后,再进行增删改操作,接着反序列化,中途的操作将会被反序列化操作所覆盖。