文章目标:理解ArrayList的add操作,明白其中的扩容机制。理解LinkedList的双向链表原理。这两个集合的源码我们都从他的构造函数入手来逐步理解整个集合的全貌
ArrayList源码
ArrayList的构造函数有多个,打开源码无参构造函数:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
}
可以看到这里对属性elementData做了一个初始化,elementData在源码中被定义为一个数组,由此可看出ArrayList的底层是基于数组实现的:
transient Object[] elementData;
而给他赋的值DEFAULTCAPACITY_EMPTY_ELEMENTDATA在代码中是一个空数组:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
通过这个无参的构造函数我们就创建了一个对象,现在对这个对象开始添加元素,add()方法为:
public boolean add(E e) {
/** 确定是否需要扩容,如果需要,则进行扩容操作*/
ensureCapacityInternal(size + 1); // Increments modCount!!
// eg1:size=0,elementData[0]="a1",然后a自增为1
elementData[size++] = e;
return true;
}
进入add方法首先要进行的是要确定是否需要扩容如果需要扩容则进行扩容,否则添加新元素进来。size记录的是当前元素的大小,添加元素的就是下标size++然后进行赋值,添加成功返回true,不成功的情况会在扩容的过程中发生,会抛出异常,接下来进入ensureCapacityInternal()来源码中扩容机制的实现:
首先调用ensureCapacityInternal()调用此函数传递进去参数的含义是elementData所需要的最小容量:
// eg1:第一次新增元素,所以size=0,则:minCapacity=size+1=1
private void ensureCapacityInternal(int minCapacity) {
// eg1:第一次新增元素,calculateCapacity方法返回值为DEFAULT_CAPACITY=10
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
在进入ensureExplicitCapacity之前要先计算集合的容量,如果当前elementData为空则直接返回默认容量大小DEFAULT_CAPACITY,默认容量大小为10,否则的话返回所需的最小容量:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// eg1:满足if判断,DEFAULT_CAPACITY=10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
第一次新增元素elementData为空且所以返回的集合容量为10,按照执行的顺序现在带着参数10进入到ensureExplicitCapacity方法,在这个方法中体现着是否需要扩容的思想,Explicit(清楚的,清晰地):
// eg1:第一次新增元素,minCapacity=10
private void ensureExplicitCapacity(int minCapacity) {
// eg1: modCount++后,modCount=1
modCount++;
/** 如果所需的最小容量大于elementData数组的容量,则进行扩容操作 */
if (minCapacity - elementData.length > 0) { // eg1:10-0=10,满足扩容需求
// eg1:minCapacity=10
grow(minCapacity);
}
}
如果是第二次进入到这个函数,则minCapacity的大小为2,2-10<0则表示不需要进行扩容,
确定了要扩容的大小开始进入到grow函数,这个函数是源码的一个亮点
// eg1:第一次新增元素,minCapacity=10,即:需要将elementData的0长度扩容为10长度。
private void grow(int minCapacity) {
/** 原有数组elementData的长度*/
int oldCapacity = elementData.length; // eg1:oldCapacity=0
/** 新增oldCapacity的一半整数长度作为newCapacity的额外增长长度 */
int newCapacity = oldCapacity + (oldCapacity >> 1); // eg1:newCapacity=0+(0>>1)=0
/** 新的长度newCapacity依然无法满足需要的最小扩容量minCapacity,则新的扩容长度为minCapacity */
if (newCapacity - minCapacity < 0) {
// eg1:newCapacity=10
newCapacity = minCapacity;
}
/** 新的扩容长度newCapacity超出了最大的数组长度MAX_ARRAY_SIZE */
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
/** 扩展数组长度为newCapacity,并且将旧数组中的元素赋值到新的数组中 */
// eg1:newCapacity=10, 扩容elementData的length=10
elementData = Arrays.copyOf(elementData, newCapacity);
}
从第二行代码可以看出,扩容机制中默认的是在原来的基础上扩大一半,但是如果扩大了一半之后仍然是小于最小容量minCapacity的话怎最终的扩容量设置为minCapacity,对于第一次即使默认的大小——10。如果要扩的容量超过了array的最大容量则进入到hugCapacity方法:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
之所以可能出现minCapacity是因为出现了溢出的情况,这时候就会报OutOfMemoryError的异常,根据抛出异常的这个时间点来说感觉可能element的实际大小是达不到MAX_ARRAY_SIZE的,自己的测试:
在此数组要扩容的大小就确定了,接下来通过Array.copyOf()来进行扩容,在此需要借鉴的是copyOf的用法,java中关于复制的四种方法参考以往的博客:
Arrays.copyOf()的源码:
public static byte[] copyOf(byte[] original, int newLength) {
byte[] copy = new byte[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
上面描述的就是ArrayList添加元素进行扩容的机制,这也是ArrayList的一个精华所在,当做remove或是get元素的时候都会有一个检查索引是否越界的情况。在上面的代码中我们看到了modCount,关于modCount的问题后期做补充:
LinkedList源码
LinkedList的无参构造函数并没有做什么特殊的操作,方法体为空,有参的构造中也是先调用无参的进行一个初始化,之后再进行一个常规的添加操作:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
对象初始化完成之后,开始添加元素:
public boolean add(E e) {
linkLast(e);
return true;
}
从代码中看出添加成功后返回的是布尔值true,linkLast函数用来将当前的这个节点添加到,这个链表的尾部:
void linkLast(E e) {
final Node<E> l = last;
// eg1: newNode null<--"a1"-->null
/** 创建一个e的Node节点,前置指向原last节点,后置指向null */
final Node<E> newNode = new Node<>(l, e, null);
/** 将newNode节点赋值为last节点 */
last = newNode;
// eg1: l=null
if (l == null) {
/** 如果是第一个添加的元素,则first指针指向该结点*/
first = newNode; // eg1: first指向newNode
} else {
/** 如果不是第一个添加进来的元素,则更新l的后置结点指向新添加的元素结点newNode*/
l.next = newNode;
}
size++;
modCount++;
}
这其中又用到了Node的这个对象,他是一个是私有化的静态内部类,在这个class中有前置,后置节点,从此构造出了一个双向链表:
private static class Node<E> {
E item; // 结点元素
Node<E> next; // 后置结点指针
Node<E> prev; // 前置结点指针
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
这个类的访问修饰符采用了private,属于一个静态内部类,它属于嵌套类的范畴,嵌套类又可以分为多种,各种嵌套类的区别与使用场景可以参考以往的博客,在这里用到的静态嵌套内部类一个主要的特征是外部可以直接创建
在LinkedList的集合中元素的添加的过程并不难,他试下的过程中的一个亮点在于的对元素的查找定位,查询指定下标的节点:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
获取元素前第一步要确定的是这个下标是否超界:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
在确定好元素的index合法之后,开始遍历查找对应下标的元素,在这个查找中采用了二分查找的思想,但是二分查找的基础是在数组上,这里根据index的位置来确定这个元素是靠近头部还是尾部,然后再进行遍历,对链表的查找这段代码的实现思路是值得的学习的:
Node<E> node(int index) {
// assert isElementIndex(index);
/** 如果需要获取的index小于总长度size的一半,则从头部开始向后遍历查找 */
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++) {
x = x.next; // 从first结点向后next查找,直到index下标node,返回node
}
return x;
} else { /** 从尾部开始向前遍历查找 */
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
x = x.prev; // 从last结点向前prev查找,直到index下标node,返回node
}
return x;
}
}