ArrayList和Linked的分析

来源:互联网 发布:淘宝卖家如何发货 编辑:程序博客网 时间:2024/05/21 18:34
写在前面的话
List接口下,主要的实现有ArrayList,Vector,LinkedList,前面两者的实现基本相同,底层都是以数组来实现,比较大的一个区别就是,ArrayList没有同步,而Vector有同步,LinkedList则是以链表来实现,因此这里只分析ArrayList和LinkedList.
以下分析基于以下版本的JDK
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

常规方法分析
从源码开始,从增、删、改的角度来看两个实现的不同之处

添加:add(E e);
ArrayList671ms764ms718msLinkedList2371ms2386ms2324ms
以上是两者各添加1000000的基本情况,从表中可以看出,LinkedList添加相对耗时,下面请看具体分析。

ArrayList的add(E e)分析
//添加的时候优先判断当前容量是否足够再次添加数据
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//确认内部容量
private void ensureCapacityInternal(int minCapacity) {
//判断是不是默认的数组,在使用ArrayList的无参构造方法时就设置数据数组为默认的数组
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);
}
//以增加当前容量的1.5倍进行扩容,扩容的时候需要复制数据到新的数组中
//如果超过Integer.MAX_VALUE则报内存溢出异常
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//以添加0.5倍长度扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新的数据容量大于最大容量
newCapacity = hugeCapacity(minCapacity);
// 耗时操作
elementData = Arrays.copyOf(elementData, newCapacity);
}

从以上源码可以看出,相对耗时点是在扩容时进行的数组复制


LinkedList的add(E e)
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++;
}
//综上所表,耗时操作在于新建对象

两者的add(int index, E element)跟add(E e)并没有多大的区别,这里不再重复


删除
//删除指定位置的数据
E remove(int index)
数据量:10000000
LinkedList:
删除位置时间100000032ms48ms33ms5000000146ms109ms117ms900000037ms41ms55ms

ArrayList:
删除位置时间10000000ms0ms15ms50000006ms19ms10ms900000013ms19ms11ms
从以上两表可以发现,删除指定位置数据的时候,LinkedList相对耗时比ArrayList多,并且LinkedList删除中间的数据比删除两端的数据需要的时间更多


//删除指定数据
boolean remove(Object obj)
数据量:10000000
LinkedList:
数据时间100000048ms33ms63ms9000000282ms251ms235ms

ArrayList:
数据时间100000032ms15ms329000000202ms173ms220ms
从以上两表的结果来看,两者在删除指定元素的时候,性能反而相近,
这是为什么?难道是我测试出问题还是其他原因?带着疑问和结论,我们一起来看看源码。

LinkedList: //删除指定位置的数据
public E remove(int index) {
//删除之前检查index的有效性
checkElementIndex(index);
return unlink(node(index));
}
//这里并没有耗时操作
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//这里也没有耗时操作
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}

//查找对应的节点,没有特殊方式,唯有遍历
//在前半段,则从前往后,在后半段,则从后往前
Node<E> node(int 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;
}
}
//取消数据在链表中的关联
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前置为null,则为第一个
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//如果后置为null,则为最后一个
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}
由以上源码所知,耗时操作在于遍历,即Node<E> node(int index)中,越往中间,耗时越久,越往两端,耗时越短。那么这里的分析跟前面的测试结果想符合,那么结论就是没错,以下继续下一个结论.

//LinkedList //删除指定对象
public boolean remove(Object o) {
if (o == null) {
//查找对应的数据
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
//同上
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
这个删除方法可以看出,查找数据都是通过遍历,那么数据越往前,则查找速度越快,越往后,则越慢,同理,删除的数据越往前,删除越快,越往后,删除越慢.
那么这个结论跟前面测试的结果也符合。

ArrayList://删除指定位置的数据
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);
//这个移动数目很关键,值越大,效率越慢,反之越快
//index越大,numMoved越小
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;

return oldValue;
}
//
E elementData(int index) {
return (E) elementData[index];
}
由以上源码所知,index越大,删除的速度越快,index越小,删除的速度越慢
换句话说,删除的数据越靠后,删除的速度越快,反之,则慢
根据这个结论,反观前面的测试数据,也能符合,虽然相差不大,但还是有区别。

ArrayList //删除指定对象
public boolean remove(Object o) {
if (o == null) {
//跟LinkedList一样,也是遍历
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//同上
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

//这里可以看到和remove(int index)是同样的道理,那么关键还是在index上
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
}
结论也是符合前面测试的结果,


//更改数据 E set(int index, E element)
LinkedList:
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//由以上源码可知,耗时操作还是在node(int index)方法中
//由此可得:越往中间,耗时越久,越往两端,耗时越短,结论一样

Node<E> node(int index):
public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
//由以上源码可知,并没有耗时操作,反而快得多,直接设置到指定的位置

对于增、删、改的总结:
经过以上分析,我们可以得出以下结论:
1、不管是直接添加数据到末尾还是添加到指定位置,LinkedList都比ArrayList需要更多的时间
2、删除指定位置的数据,LinkedList,删除的时候越靠近中间,删除时间越长,越靠近两端,删除时间越少,而ArrayList则不是,删除的时候越靠后,删除的时间越长
3、删除指定数据,则两者相似,都是越往后,删除时间越长
4、至于更改数据,LinkedList也是越靠近中间,耗时越久,越往两端,耗时越少,而对于ArrayList来说,几乎没有什么耗时,可以直接更改










原创粉丝点击