ArrayList
构造方法
1、ArrayList():无参构造方法创建的ArrayList起始容量为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2、ArrayList(int initialCapacity):会使用指定容量的数组
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);
}
}
3、ArrayList(Collection<? extends E> c):给定一个集合创建ArrayList,容量为这个集合的大小
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
扩容机制
list.add(Object o)触发的扩容
-
第一次扩容是在存入第一个元素时,容量为10
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
-
当存满的时候再添加一个元素就会再次扩容,扩容为 INT(原始容量/2)+原始容量==INT(原始容量*1.5)
list.addAll(Collection c)触发的扩容
(添加的元素个数+原始容量)和下一次扩容的大小进行比较,选一个最大值
没有元素时,扩容为Math.max(10,实际元素个数),有元素时为Math.max(原容量1.5倍,实际元素个数)
比如原始容量为6,现在添加5个元素进去,就得到了11个元素,和下一次扩容的大小15比较,选择15
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);
}
迭代器
fail-fast (ArrayList)
一旦发现遍历的同时集合被修改,则立即抛异常。遍历的同时不能修改,尽快失败。
java.util.ConcurrentModificationException 并发修改异常
-
利用增强for循环遍历ArrayList首先创建迭代器
public Iterator<E> iterator() { return new Itr();}
-
迭代器里面有一个成员变量expectedModCount = modCount
expectedModCount 是迭代器里的成员变量,表示集合被修改的次数,刚开始迭代时记录集合的修改次数
modCount是集合里的成员变量,也表示集合被修改的次数
-
每次迭代都会调用next()方法移动到下一个元素,并调用checkForComodification()
将最开始记录的集合修改次数与现在集合的修改次数进行比较,若不相等则直接抛出异常
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]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
fail-safe (CopyOnWriteArrayList)
发现遍历的同时集合被修改,牺牲一致性来让整个遍历运行完成。遍历的同时可以修改,原理是读写分离
-
创建迭代器对象
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0);}
-
将数组保存在迭代器中
private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; // 迭代器创建时保存的数组 }
-
当外部修改集合时,并不会影响到迭代器中的数组遍历
-
因为他在创建迭代器的时候就已经将集合中的数组快照到了迭代器的成员变量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; setArray(newElements); return true; } finally { lock.unlock(); } }
-
并不会影响到迭代器中的遍历。因此牺牲了一致性来让整个遍历运行完成
-
ArrayList vs LinkedList
-
ArrayList
-
基于数组,需要连续内存
-
随机访问快(根据指定下标访问)
实现了 RandomAccess接口,标志性接口,实现了这个接口,底层就会根据索引下标进行寻找
-
尾部插入、删除性能可以,其他部分插入、删除都会移动数据,因此性能会低。
-
可以利用cpu缓存,局部性原理
-
-
LinkedList
- 基于双向链表、无需连续内存
- 随机访问慢(沿着链表遍历)
- 头尾插入删除性能高,其他部分性能低下。
- 占用内存多,不能利用cpu缓存局部性原理,速度慢。
-
比较:
- 1000个随机数,访问中间索引:
ArrayList Win
- 头部插入:
LinkedList Win
- 尾部插入:
ArrayList Win
- 1000个随机数,插入中间索引位置:
ArrayList Win
- 1000个随机数,访问中间索引:
在实际开发中,绝大部分都是用ArrayList
CPU缓存的局部性原理
一个优秀的程序通常具有良好的局部性,它们通常会重复使用已用过的数据,或者使用已用过数据的邻近数据,也就是说,程序常常会使用集中在一起的局部数据。
**局部性分为:时间局部性和空间局部性。**如果一个内存位置被重复的引用,那就是有了时间局部性,如果一个内存位置被引用了,很快这个位置的附近位置也被引用了,这就有了空间局部性。
由于缓存中的数据是一个个数据块,每个数据块包含几十到几千字节不等,如果某个程序要访问数组a,第一次缓存没命中,cpu会从主存中取出包含数组a的一个数据块,复制到缓存中来,下次访问a[1],a[2],a[3]的数据时每次都缓存命中,极大的提高了效率,实现了空间的局部性。