transient Object[] elementData; // 用于存储数据的buffer数组
在 ArrayList
集合的底层是由一个可变的动态 Object[]
数组组成的,由于他是由数组构成的,所以该集合类框架对于随机访问的效率很高,时间复杂度为 O(1)
。
public boolean add(E e) {
modCount++;// 结构更改次数
add(e, elementData, size);// 套娃
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();// 自动扩容
elementData[s] = e;// 在数组的末尾插入元素
size = s + 1;
}
但是在插入元素的时候,如果采用默认的 add(E e)
便会在数组的末端插入,时间复杂度近似为 O(1)
public void add(int index, E element) {
rangeCheckForAdd(index);// index校验
modCount++;// 结构更改次数
final int s;
Object[] elementData;
// 当size 与 数组的容量一致,执行自动扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();// 扩容
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);// 数组元素往后移动一位,腾出桶位给待插元素
elementData[index] = element;
size = s + 1;
}
如果在数组中间插入一个元素,那么他会先将该位置后面的元素整体往后移动一位后,再把待插入元素插入,这时的时间复杂度就近似为 O(n)
。
纵观 ArrayLis
的源码,所有的方法均没有进行线程安全方面的设计,故可见他是一个线程不安全的集合,与之相对应的就是 Vector
,该集合框架,他就是一个线程安全的,对其内部每一个方法均进行线程安全的控制。根据以上的一些信息我们可以得出:在单线程的情况下,我们使用 ArrayList
的效率会比 Vector
高很多,但是在多线程的情况下,使用 ArrayList
会有线程不安全的风险。
在 ArrayList
中,其扩容的方法主要为 grow()
方法:
我们进入内部:
private Object[] grow() {
return grow(size + 1);
}
我们发现他其实是一个套娃的方法:
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
其实到这里我们即可看出一点猫腻了, 用于储存数据的数组会被复制给另外一个数组,并把另一个数组的引用赋值给 elementData
,而新数组的大小就是扩容后 elementData
数组的大小,这个大小由方法 :newCapacity(int minCapacity)
确定。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);// 1.5 倍扩容
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow,该值过大,超过了 int 的表示范围,为负数
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0) //当newCapacity>MAX_ARRAY_SIZE时,返回 hugeCapacity
? newCapacity
: hugeCapacity(minCapacity);
}
看到这里我们神秘的扩容机制将不再神秘了,细细分析,其实核心的还是最后一个 return
语句:
return (newCapacity - MAX_ARRAY_SIZE <= 0) //当newCapacity>MAX_ARRAY_SIZE时,返回 hugeCapacity
? newCapacity
: hugeCapacity(minCapacity);
现在,newCapacity = 1.5*oldCapacity
,它和定义的最大的数组大小 MAX_ARRAY_SIZE
进行比较,如果小于,则返回 newCapacity
进行扩容,否则调用 hugeCapacity(int minCapacity)
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
比最大的数组大小大的时候,返回 int
所能表达的最大整数 Integer.MAX_VALUE
,否则,返回最大数组大小 MAX_ARRAY_SIZE
。到此, ArrayList
的扩容机制,基本就搞清楚了。
我们来总结一下:
变量说明:
newCapacity
:新计算的扩容大小
minCapacity
:最小的所需数组大小
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
:最大的数组大小(不同的JVM
在数组的处理上,可能会有头字符,所以预留了8个位置)
针对 ArrayList
而言,会在往集合中添加元素的时候进入到扩容入口函数 grow()
,当当前集合中的元素数量与存储数组的长度一样的时候,会触发扩容机制,会把当前数组的长度 + 1 作为扩容判断的标准,并计算出新的大小 newCapacity
(约为当前数组长度的1.5倍),主要分为以下几种情况:
newCapacity <= minCapacity
,判断当前的数组是否为默认的空数组,如果是,就返回默认初始化大小值(10
)与 minCapacity
中大的作为实际的扩容大小;否则直接返回 minCapacity
。newCapacity
不超过最大数组大小 MAX_ARRAY_SIZE
,直接返回 newCapacity
作为扩容大小,否则,用 minCapacity
和 MAX_ARRAY_SIZE
比较,如果不超过MAX_ARRAY_SIZE
,则返回MAX_ARRAY_SIZE
作为扩容大小;否则返回 Integer.MAX_VALUE
作为扩容大小。总之,在一般的情况下,基本都是以近乎于原数组大小的1.5倍扩容。
补充:
ArrayList
的数组是在第一次添加元素的时候将数组设置为默认的初始化大小的,是一种懒加载方式。
protected Object[] elementData;
和 ArrayList
的底层数据结构一样,都是利用一个可变大小的动态 Objec[]
数组来存储数据结构。他和 ArrayList
有着类似的性质,这里不再赘述,但是和 ArrayList
不同的是:他是线程安全的,可以在多线程的条件下,安全地访问一个 Vector
对象 ,不过他的访问效率比 ArrayList
低;他也不是和 ArrayList
类似的懒加载方式:
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
他提供的构造函数,都是套娃上面这个方法,在构造对象的时候就已经给数组申请到了一定的空间。无参构造时,默认大小也是 10
,同时还可以提供自定义的扩容增加数量 capacityIncrement
。
由于他和 ArrayList
的机制类似,其他的代码,这里不再展示,只关心关键方法 newCapacity(int minCapacity)
:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 在原来的数组大小的基础上加上扩容量,默认值为0
// 故在没有给定自定义扩容量或者扩容量给定为0时,
// 默认扩容到原来的 2 倍。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
总体来看,扩容机制和 ArrayList
类似,只是新的大小计算方式不同。
见名知义,是采用的 链表
这种数据结构,来作为存储数据的数据结构。
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;
}
}
LinkedList
的底层实际上是采用的双向链表。(JDK1.6
之前是循环链表,在 JDK1.7
取消了)。
为啥要采用双向链表?
因为在链表这种数据结构中查询一个元素,必须从头结点开始一次遍历,并且他在内存空间中是一种内存地址不连续的存储方式,寻址代价比较大,如果链表比较长,那么查询效率就会比较低,采用双向链表,有两个首位节点,,可以从两个方向开始遍历链表,在查询遍历的时候,一定程度上可以提高一定的效率。举个源码中的例子:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
另外,和 ArrayList
一样,是一种线程不安全的集合,如果进行了线程同步,那么该结构的访问效率将进一步降低。