ArrayList 与 LinkedList
前言
ArrayList 与 LinkedList 估计在java面试汇总肯定会问到,一般需要掌握的点有.
比如还应该了解的有:
- 都是List,都java集合Collection接口的实现类.
- ArrayList基于数组实现,长度固定,但可以扩容.
- ArrayList查询快,增删慢,因为需要移动数据
- LinkedList增删快,不用移动数据
- LinkedList 提供了更多的方法来操作其中的数据,但是这并不意味着要优先使用LinkedList ,因为这两种容器底层实现的数据结构截然不同。
- 二者线程多不安全.为什么?
所以本文将试图去源码上找答案.
ArrayList
ArrayList是可变长度数组-当元素个数超过数组的长度时,会产生一个新的数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中。
方法及源码
- add
//底册Object类型的数组
transient Object[] elementData; // non-private to simplify nested class access
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 扩容
elementData[size++] = e; //扩容后直接index赋值
return true;
}
先看是否越界,越界就扩容,
扩容源码如下
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); //通过拷贝放入新数组进行扩容
}
这里Arrays.copyOf 底层使用System.arraycopy拷贝到新数组(len+1)里.
- get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
get直接通过index获取值,so快.
- 修改
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
没啥好讲的,直接覆盖.
- 删除
public E remove(int index) {
rangeCheck(index); //检查边界
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, //拷贝new数组
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
删除后把index+1前移.然后末尾置为null,这里注释交给GC不太懂. 有一次拷贝和移动数组.
线程不安全
- 为什么会线程不安全
- 因为size++属于非原子操作,这里在多线程下可能会被其他线拿到,又没有做扩容处理 可能会值覆盖或者越界即ArrayIndexOutOfBoundsException.
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 如果两个线程擦操作同一个下标数据,可能会覆盖. 原因是a线程已经扩容了,b线程没有get到size的变化,直接坐了覆盖操作.
- 如何保证线程安全
首先理解线程安全,就是保证多个线程下数据
- Collections.synchronizedList
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);} //同步
}
但是源码注释有写到,遍历集合的时候需要手动同步
* It is imperative that the user manually synchronize on the returned
* list when iterating over it:
* <pre>
* List list = Collections.synchronizedList(new ArrayList());
* ...
* synchronized (list) {
* Iterator i = list.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
所以为了防止忘记,建议第二种.
- 使用CopyOnWriteArrayList
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();
}
}
LinkedList
LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下:
- add
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode; //直接向后追加
size++;
modCount++;
}
- remove
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next; // 将前一节点的next引用赋值为x的下一节点
x.prev = null; //解除了x节点对前节点的引用
}
if (next == null) {
last = prev;
} else {
next.prev = prev; // 将x的下一节点的previous赋值为x的上一节点
x.next = null; //解除了x节点对后节点的引用
}
x.item = null; // 将被移除的节点的内容设为null
size--;
modCount++;
return element;
}
删除过程基本上就是:
- 前节点指向后节点
- 当前pre为空
- 后节点的pre指向前节点
- 当前节点next为空
- 当前节点为空 交给gc处理
- size--
与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。
- get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// 获取index处的节点。
// 若index < 双向链表长度的1/2,则从前先后查找;
// 否则,从后向前查找。
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与长度size的一半比较,要么从头开始,要么从尾部开始. 但是都需要挨个查找,所以这里体验出LinkedList比ArrayList查询效率慢了
多线程下
LinkedList源码中发现,同样会出现非原子性操作 size++/size--问题,所以是线程不安全的.
- Collections.synchronizedList 通ArrayList同步方法
- ConcurrentLinkedQueue 这个源码下详解
总结
存储结构
ArrayList是基于数组实现的;按照顺序将元素存储(从下表为0开始),删除元素时,删除操作完成后,需要使部分元素移位,默认的初始容量都是10.
LinkedList是基于双向链表实现的(含有头结点)。
线程安全性
ArrayList 与 LinkedList 因为都不是原子操作,都不能保证线程安全.
Collections.synchronizedList,但是迭代list需要自己手动加锁
ps:
Vector实现线程安全的,即它大部分的方法都包含关键字synchronized,但是Vector的效率没有ArraykList和LinkedList高。
效率
ArrayList 从指定的位置检索一个对象,或在集合的末尾插入、删除一个元素的时间是一样的,时间复杂度都是O(1)
LinkedList中,在插入、删除任何位置的元素所花费的时间都是一样的,时间复杂度都为O(1),但是他在检索一个元素的时间复杂度为O(n).
ps: 事实上lindlinst并不会节省空间,只是相比增删节省时间而已.