您的位置:首页 > 编程语言 > Java开发

java集合类(二)List之ArrayList

2016-07-11 18:01 399 查看

ArrayList概述:

  ArrayList是List接口基于数组的实现。它允许包括 null 在内的所有元素。每个ArrayList实例都有一个容量,该容量代表可以存储元素的多少。每个ArrayList实例都有一个初始大小,当然你也可以通过构造方法指定初始大小。随着向ArrayList中不断添加元素,其容量也自动增长。当元素数量达到当前容量最大值时会导致数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 但是需要注意,此方法的实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

底层探究

1 初始容量

private static final int DEFAULT_CAPACITY = 10;


2 底层存储

/**
*Object类型数组
*/
transient Object[] elementData;


3 构造方法

/**
* 指定初始容量
*/
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);
}
}

/**
* 默认构造,初始容量为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* 使用一个集合初始化一个ArrayList
*/
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;
}
}


4 存储

  ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection

//设置指定位置元素的值(可理解为更新操作)
set(int index, E element)


//往集合中添加元素,如果容量不足则扩容
add(E e)


//往指定位置插入元素,如果容量不足则先扩容。原先index及之后的元素均往后移动一位
add(int index, E element)


5 删除

//删除指定位置的元素,原先index之后的元素均往前移动一位
public E remove(int index) ;


//移除此列表中首次出现的指定元素(如果存在),此操作需要遍历数组
public boolean remove(Object o) {


以上介绍了ArrayList的增删等操作,下面再来看下源码

仔细观察我们会发现还有两个空数组

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};


这两个空数组有什么用呢?我们再把上面的两个构造方法拿来看下:

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);
}
}


我们可以看到,当使用默认构造方法时,直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给了elementData,当使用指定容量进行构造ArrayList时,如果initialCapacity=0,将EMPTY_ELEMENTDATA赋值给了elementData,这两种方式构造出来的ArrayList初始容量均为0,那么为什么要使用两个不同的空数组呢?想知道答案我们来看下add(E e);方法:

public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}


从上面代码可以看到,在add一个元素时,才对容量进行了操作,为什么要把扩容操作放在这里呢?其实是防止你new出一个数组,但是不用,导致空间资源的浪费。回到上面的问题,我们看下ensureCapacityInternal方法:

private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}


看下这里有个if判断,当elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,对容量有一个赋值操作,minCapacity为DEFAULT_CAPACITY与minCapacity的最大值。显然当第一次add元素时minCapacity==1所以初始容量就为DEFAULT_CAPACITY也就是10了。所以使用两个不同的空数组的原因就是为了保证:当我们使用默认构造方法对ArrayList进行构造时,初始容量为10。

  下面再来看下ensureExplicitCapacitygrow方法:

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//当最小所需容量大于当前数组容量时进行grow操作
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//先尝试新容量为旧熔炼个的1.5倍
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);
}


  可以看出,当最小所需容量大于当前数组容量时进行grow操作。先尝试新容量为旧熔炼个的1.5倍,如果还不够,那么就使用minCapacity作为当前扩充的容量大小。

  此外有没有注意到ensureExplicitCapacity方法中有一个modCount++的操作,这个modCount是干什么用的呢?

  

  ArrayList的modCount是从类 java.util.AbstractList 继承的字段:

protected transient int modCount


这个字段代表已从结构上修改此列表的次数。从结构上修改是指更改列表的大小(也就是元素个数发生了变化),或者打乱列表,从而使正在进行的迭代产生错误的结果。

此字段由 iterator 和 listIterator 方法返回的迭代器和列表迭代器实现使用。如果意外更改了此字段中的值,则迭代器(或列表迭代器)将抛出 ConcurrentModificationException 来响应 next、remove、previous、set 或 add 操作。在迭代期间面临并发修改时,它提供了快速失败 行为,而不是非确定性行为。

大家可以测试下下面这段程序:

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
if (integer == 10)
list.remove(integer);  //注意这个地方,此处换成list.add(integer)结果是一样的
}
}


上面这段代码会抛出如下异常:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at Main.main(Main.java:12)


为何会报这个错?看下ArrayListIterator源码(有部分代码未贴出):

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();
}
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}


我们可以看到当做next()、remove()操作时,都要先checkForComodification()而这个方法做的事情就是判断modCount 与expectedModCount是否相等。不相等就报错。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: