ArrayList个LinkedList的区别
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- 随机获取参数,ArrayList速度高于LinkedList。
- 对于增删,ArrayList需要选择内存片区重新建立新数组,相比LinkedArray优势比较明显。
数组指定长度和不指定长度影响
public class ListTest {
public static int length = 2000000;
public static List<String> listNoLength = new ArrayList<>();
public static List<String> listLength = new ArrayList<>(length);
public static void addList(int sign){
long start = System.currentTimeMillis();
for (int i = 0; i < length; i++) {
if(sign==0){
listNoLength.add("YoniYuan");
}else{
listLength.add("YoniYuan");
}
}
long end = System.currentTimeMillis();
System.out.println("sign: "+ sign+" "+(end - start));
}
public static void main(String[] args) {
addList(0);
addList(1);
}
}
细节优化:长度尽量使用2的幂,计算机分配空间大都使用次幂去分配,减少水平空间。
存储、扩容
数组
数组从5扩容到8流程
- 新增数组空间判断。判断有没有空闲空间存储新数组。
- 申请连续空间。
- 复制老数组。
- 增加内容。
- 删除老数组。
链表
链表扩容流程
- 不需要连续空间。
- 大小不定。
时间复杂度
- 同样查找O(n)数组遍历比链表快。
- 数组是连续内存,会有一部分或全部数据一起进入到CPU缓存,而链表还需要再去内存中根据上下标查找。CPU缓存比内存块。
- 数组大小固定,不适合动态存储,动态添加,内存为一连续的地址,可随机访问,查询快。而链表大小可变,扩展性强,只能顺着指针的方向查询,速度较慢。
源码分析
尾部添加
ArrayList
transient Object[] elementData;
private int size;
protected transient int modCount = 0;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保有足够的空间
elementData[size++] = e; // 完成添加
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
}
结论:如果容量足够大,add()操作效率还是很高的,当需要扩容时会进行大量数组复制操作,从而影响效率。modCount变量用于在遍历集合时,检测是否发生了add,remove。
LinkedList
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
结论:LinkedList不需要扩容,对比ArrayList来说是有优势的。
随机插入
ArrayList
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
// Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
结论:每次操作都会有大量的数组复制,从而导致性能下降。
LinkedList
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) // 链表尾部添加
linkLast(element);
else // 链表中间添加
linkBefore(element, node(index));
}
结论:与add(E e)是一样的,不会导致性能下降。
删除元素
ArrayList
public boolean remove(Object o) {
if (o == null) { // 移除对象数组中的第一个null
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else { // 移除对象数组中的第一个o
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index, numMoved); // 删除元素之后的元素整块前移
elementData[--size] = null; // clear to let GC do its work
}
// 删除指定索引index下的元素,返回被删除的元素
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, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
结论:删除任意位置元素都需要数组重建。而且remove(Object o)需要重建数组,而remove(int index)不需要。
LinkedList
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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; // 将前驱节点的后继节点指向后继节点
x.prev = null;
}
if (next == null) {
last = prev; // 删除尾节点,令尾节点指向该节点的前驱节点
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
结论:首先通过循环找到元素。如果处于前半段则从前循环,如果处于后半段,则从后循环。如果处于中间位置,则效率较低。