是的,这又是一篇转文
讲道理,有时候转载博文也是挺效率得一件事,毕竟没有那么多的时间去自己抠(哎),毕竟我是站在dalao得肩膀上啊ArrayList的实现原理侵删
本篇是研究一下ArrayList的底层实现,顺便把List的底也给掀了
ArrayList概述
ArrayList是List接口的可变数组的实现。实现了所有可选列表的操作,并允许包括null在内的所有元素。同时ArrayList也有内部的方法用于操作数组大小以及元素。
每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
另外需要提及的一点就是,ArrayList并不是线程安全的,多线程使用时需要注意注意同步。
来看一下源码
其实本质就是对数组的操作,我们在使用时的简单操作的原理是透明的。
存储实现
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
构造方法
ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表,构造一个指定初始容量的数组,以及构造一个包含指定Collection元素的列表,这些元素按照collection的迭代器返回它们的顺序。
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
存储
常见的添加、替换方法
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
RangeCheck(index);
// 有一个强制转型的操作
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1); // Increments modCount!!
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
某些细节
看到这里我注意到有两个方法出现的次数很高:ensureCapacity()
和System.arraycopy()
读取
// 返回此列表中指定位置上的元素。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
删除
指定下标删除或者指定元素删除
// 移除此列表中指定位置上的元素。
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。
public boolean remove(Object o) {
// 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 类似remove(int index),移除列表中指定位置上的元素。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
注意
元素被移除后,该元素后面的元素的下标值都减一了,使用时需要注意
调整数组容量
每次对ArrayList进行操作之前都会判断一下容量的大小,如果容量不够,会及时的自动扩充。但是通过我们手动调用,给ensuerCapacity输入参数,可以避免方法重复递归调用
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
可以明显的看出,数组的每次自我扩容是原来的1.5倍,较为保守。
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
Fail-Fast机制(快速失败机制)
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
ArrayList值得看一看的基本就在这里,其余的方法使用时可自行查阅API文档。(所以说,这就是一个自动的可以存放任意类型对象的数组,使用时很方便)
那就不得不说一下Vector
打开Vector源码一看,果不出期然,Vector的实现与ArrayList的底层实现基本如出一辙,除了线程安全基本没有改变什么(重量级操作)
其实查看源码时,很容易注意到,很多方法都用synchronized
修饰了,这也就是线程安全的实现原理。
顺便看一看LinkedList
虽然实现了List的接口,但底层并不是对数组进行的操作,而是使用双向循环链表实现。继承于AbstractSequentialList,可以被当作堆栈、队列或双端队列进行操作。
FROM-Java集合—LinkedList源码解析侵删
链表节点Entry
其中的链表节点元素都是Entry类型的实例(包含三个变量:previous、next、element)
private static class Entry<E> {
E element; // 业务数据
Entry<E> next; // 后节点信息
Entry<E> previous; // 前节点信息
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
构造方法
因为是双向-循环-链表
第一种构造方法在初始化时只有头元素的情况下,previous和next都指向自己,形成一个闭环,这是称为循环的原因。
第二种方法是接收一个Collection参数c,调用第一种方法构造一个空链表(首节点不算),然后通过addAll()方法将c中的元素全部添加到链表中。
插入
这里是重头戏,dalao已经讲解的很好了,丝毫不敢稍有改动。
- 初始化后LinkedList是首结点闭环
- 再初始化一个预添加的Entry实例,
Entry newEntry = newEntry(e, entry, entry.previous);
- 调整新加入节点和首节点的前后指针
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
再有新的节点插入时,过程是这样的
- 新建一个节点
- 修改前后指针
清除
public void clear() {
Entry<E> e = header.next;
// e可以理解为一个移动的“指针”,因为是循环链表,所以回到header的时候说明已经没有节点了
while (e != header) {
// 保留e的下一个节点的引用
Entry<E> next = e.next;
// 解除节点e对前后节点的引用
e.next = e.previous = null;
// 将节点e的内容置空
e.element = null;
// 将e移动到下一个节点
e = next;
}
// 将header构造成一个循环链表,同构造方法构造一个空的LinkedList
header.next = header.previous = header;
// 修改size
size = 0;
modCount++;
}
删除
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
// 保留将被移除的节点e的内容
E result = e.element;
// 将前一节点的next引用赋值为e的下一节点
e.previous.next = e.next;
// 将e的下一节点的previous赋值为e的上一节点
e.next.previous = e.previous;
// 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用
e.next = e.previous = null;
// 将被移除的节点的内容设为null
e.element = null;
// 修改size大小
size--;
modCount++;
// 返回移除节点e的内容
return result;
}
简单说就是,先把前后节点互相连接起来,再清空该节点的所有数据。然后等待GC回收即可
get()方法
感觉这是一个需要关注一点的地方
// 获取双向链表中指定位置的节点
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
// 获取index处的节点。
// 若index < 双向链表长度的1/2,则从前先后查找;
// 否则,从后向前查找。
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
一个小细节就可以提高性能
总结
其实就一个数组/链表,基本原理就是这样子,无非是Java给你封装好了直接拿来用就好了┑( ̄Д  ̄)┍
把这个List更完,常用的集合框架就只剩一个TreeMap 了,由于底层是使用红黑树实现的(虽然看了好几遍插删,但是记不住啊),日后补完JVM垃圾回收在去干它好了。