目录
ArrayList
ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。底层使用数组实现。类的属性中核心的属性为elementData,类型为Object[],用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候,此字段是不会被序列化的。
该集合是可变长度数组,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过方法ensureExplicitCapacity(int minCapacity)来实现。数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。若是能预估到顶峰容量,可以设置一个足够大的量以避免数组容量以后的扩展。add函数源码:
public boolean add(E e) {
// 添加元素
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
说明:在add函数中有ensureCapacityInternal,此函数可以理解为确保elementData数组有合适的大小。ensureCapacityInternal的具体函数如下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 判断元素数组是否为空数组
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取较大值
}
ensureExplicitCapacity(minCapacity);
}
在ensureCapacityInternal函数中有ensureExplicitCapacity函数,这个函数也是为了确保elemenData数组有合适的大小。ensureExplicitCapacity的具体函数如下:
private void ensureExplicitCapacity(int minCapacity) {
// 结构性修改加1
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在ensureExplicitCapacity函数又发现了grow函数,grow函数才会对数组进行扩容,ensureCapacityInternal、ensureExplicitCapacity都只是过程,最后完成实际扩容操作还是得看grow函数,grow函数的具体函数如下:
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
if (newCapacity - minCapacity < 0) // 新容量小于参数指定容量,修改新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大于最大容量
newCapacity = hugeCapacity(minCapacity); // 指定新容量
// 拷贝扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
如果在添加的时候原数组是空的,就直接给一个10的长度,否则的话就加一。正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。当调用add方法时,实际上的函数调用如下:
Java容器的快速报错机制ConcurrentModificationException
List的fail-fast机制,采用modCount实现,能够防止多个进程同时修改同一个容器的内容。如果在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入,删除或修改此容器的某个对象,就会立刻抛出ConcurrentModificationException。
在迭代遍历的过程中都调用了方法checkForComodification来判断当前ArrayList是否是同步的。假设往一个Integer类型的ArrayList插入了10条数据,那么每操作一次modCount(继承自父类AbstractList)就加一所以就变成10,而当对这个集合进行遍历的时候就把modCount传到expectedModCount这个变量里,然后ArrayList在checkForComodification中通过判断两个变量是否相等来确认当前集合是否是同步的,如果不同步就抛出ConcurrentModificationException。所谓的不同步指的就是,如果在遍历的过程中对ArrayList集合本身进行add,remove等操作时候就会发生。当然如果用的是Iterator那么使用它的remove是允许的,因为此时直接操作的不是ArrayList集合而是它的Iterator对象。
LinkedList
LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。底层的数据结构是基于双向链表的,该数据结构称为节点。双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。
node函数,此函数是根据索引下标找到该结点并返回,具体代码如下:
Node<E> node(int index) {
// 判断插入的位置在链表前半段或者是后半段
if (index < (size >> 1)) {
// 插入位置在前半段
Node<E> x = first;
for (int i = 0; i < index; i++) // 从头结点开始正向遍历
x = x.next;
return x; // 返回该结点
} else {
// 插入位置在后半段
Node<E> x = last;
for (int i = size - 1; i > index; i--) // 从尾结点开始反向遍历
x = x.prev;
return x; // 返回该结点
}
}
它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找。根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。
对addAll函数的思考
addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,这里对addAll(int, Collection<? extends E>)型进行分析。
// 添加一个集合
public boolean addAll(int index, Collection<? extends E> c) {
// 检查插入的的位置是否合法
checkPositionIndex(index);
// 将集合转化为数组
Object[] a = c.toArray();
// 保存集合大小
int numNew = a.length;
if (numNew == 0) // 集合为空,直接返回
return false;
Node<E> pred, succ; // 前驱,后继
if (index == size) {
// 如果插入位置为链表末尾,则后继为null,前驱为尾结点
succ = null;
pred = last;
} else {
// 插入位置为其他某个位置
succ = node(index); // 寻找到该结点
pred = succ