概述
List 是重要的数据结构之一。最常用的的便是: ArrayList、Vector 和 LinkedList 三种了,他们类图如下图所示:
由上图可知,这三种 List 都实现了 Collection 和 List 接口。
这三种不同的实现中,ArrayList 和 Vector 使用了数组实现,封装了对内部数组的操作。它们俩唯一的区别是 ArrayList 没有任何一个方法是同步的,而 Vector 则是做了线程同步。我们之后均以 ArrayList 为例进行讲解。
LinkList 使用了双向链表数据结构,并且维护了 first 和 last 两个成员变量来指示链表的头和尾。这和 ArrayList 是截然不同的实现技术,也决定了它们适用于不同的场景中。
LinkedList 是由一系列表项连接而成的。一个表项总是分为三个部分,分别是:元素内容,前驱表项 和 后驱表项,如下图所示:
在 JDK 的视线中,不管 LinkedList 是否为空,链表中总是有一个 header 表项,它即表示链表的开始,也表示链表的结尾。
下面以增加和删除为例,比较 ArrayList 和 LinkList 的不同之处。
1、增加元素到列表尾端
在 ArrayList 中增加元素到队列端尾的代码如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 确保内部数组有足够的空间
elementData[size++] = e;
return true;
}
/**
* 分配的最大内存。
* 有些虚拟机会为数组保留了一些头部内容,试图分配更多的空间会引起内存溢出
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //计划扩容到现在容量的1.5倍
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); //进行新旧数组的复制
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
可以看到,只要 ArrayList 的当前容量很大,add() 操作的效率是非常高的。只有当 ArrayList 对容量的需求超过当前数组的大小时,才需要扩容。扩容过程中,会进行大量的数组复制操作。当数组复制时,最终调用 Arrays.copyof() 方法。
LinkedList 的 add() 操作实现如下,它将任意元素增加到队列的尾端:
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++;
}
linkLast() 方法将 e 插入到链表的末尾。可见,LinkedList 由于使用了链表的结构,因此不需要维护容量的大小。从这点上,它比 ArrayList 有一定的性能优势,
然而每次元素增加都要新建一个 Entry 对象,并进行更多的赋值操作。在频繁的系统调用中,对性能有一定的影响。
分别使用 ArrayList 和 LinkedList 运行以下代码:
public class ListTest {
public static void testArrayList() {
Object obj = new Object();
List list = new ArrayList<>();
for (int i = 0; i < 500000; i++) {
list.add(obj);
}
}
public static void testLinkedList() {
Object obj = new Object();
List list = new LinkedList<>();
for (int i = 0; i < 500000; i++) {
list.add(obj);
}
}
public static void main(String[] args){
testArrayList();
testLinkedList();
}
}
使用 TPTP 分析运行结果为:
可以看到,ArrayList 的性能比 LinkedList 性能高出 4 倍左右。
2、增加元素到列表任意位置
在 ArrayList 中,任意位置插入的实现代码如下:
public void add(int index, E element) {
rangeCheckForAdd(index); //数组越界检查
ensureCapacityInternal(size + 1);
// 增加modCount值
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //index 之后的元素都需要后移一个单位
elementData[index] = element;
size++;
}
可以看到,每次插入操作,都会进行一次数组的复制。而这个操作在增加元素到 List 尾端的时候是不存在的。大量的数组重组操作会导致系统性能低下。并且,插入的元素在 List 中的位置越靠前,数组重组的开销也越大。
而LinkedList 此时就显示出了优势:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
可见,对于 LinkedList 来说,在 List 尾端插入数据与在任意位置插入数据是一样的。并不会因为插入的位置靠前而导致插入方法的性能降低。现在在极端情况下对他们的这个方法进行测试,每次都将元素插入到 List 的最前端。
public class ListTest {
public static void testArrayList() {
Object obj = new Object();
List list = new ArrayList<>();
for (int i = 0; i < 50000; i++) {
list.add(0, obj);
}
}
public static void testLinkedList() {
Object obj = new Object();
List list = new LinkedList<>();
for (int i = 0; i < 50000; i++) {
list.add(0, obj);
}
}
public static void main(String[] args) {
testArrayList();
testLinkedList();
}
}
运行以上代码,执行时间如下:
然后每次都插入中间位置:
list.add(list.size()>>1, obj);
性能结果如下:
我们再插入末尾位置:
list.add(list.size(), obj);
性能结果如下:
因此通过对比以上三种情况下的插入操作,我们得到如下结论:当插入的位置靠前的时候,ArrayList 的性能是由于 LinkedList 的。而当插入的位置越来越靠后, ArrayList 的性能变得比 LinkedList 越来越好。
这是因为: ArrayList 的性能损耗主要在于每次插入需要复制数组,LinkedList 的性能损耗则在循环遍历链表找插入位置元素的上面。当插入位置靠前的时候,ArrayList 会花费大量的时间在复制数组上面,而 LinkedList 遍历链表找插入位置的时间消耗确是最少的。随着插入位置越来越靠后, ArrayList 需要复制的数组越来越小,消耗的时间越来越少。而 LinkedList 寻找的时间越来越多(确切的说,当插入位置为中间的时候,LinkedList 寻找的时间是最多的 )或者说是比不上 ArrayList 复制数组时间的减少速度。
3、删除任意位置元素
对于 ArrayList 来说, remove() 方法和 add() 方法雷同。在任意位置移除元素后,都要进行数组的重组。实现如下:
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; // Let gc do its work
return oldValue;
}
可以看到,在 ArrayList 的每一次有效地元素删除操作后,都要进行数组的重组。并且删除的位置越靠前,数组重组时的开销也越大;要删除的元素越靠后,开销越小。
对于 LinkedList ,删除操作的实现如下:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
}
}
主要的时间消耗也是在寻找删除位置元素上面。
删除的性能对比和增加的性能对比可以参照插入操作的性能对比。
4、容量参数
容量参数是ArrayList 和 Vector 等基于数组的 List 的特有性能参数,它表示初始化数组大小。每一次数组元素数量超过其现有大小的时候,它表会进行一次扩容,数组的扩容会导致整个数组进行一次内存复制。因此合理的数组大小有助于减少数组扩容的次数,从而提高系统性能。
默认情况下, ArrayList 的数组初始大小为10,每次进行扩容将新的数组大小设置为原来的1.5倍。
5、遍历列表
在 JDK1.5 之后,至少有三种遍历的方式:ForEach、迭代器、for循环。
package bupt.xiaoye.charpter2.list;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestFor {
public static void testForEach(List list) {
Object temp;
for(Object t : list)
temp = t;
}
public static void testFor(List list) {
Object temp;
for (int i = 0; i < 1000000; i++) {
temp = list.get(i);
}
}
public static void testIterator(List list) {
Object temp;
for(Iterator<Object> it = list.iterator();it.hasNext();){
temp = it.next();
}
}
public static void main(String[] args) {
Object obj = new Object();
List list = new ArrayList();
for (int i = 0; i < 1000000; i++) {
list.add(obj);
}
testFor(list);
testForEach(list);
testIterator(list);
}
}
运行结果为:
可以看到,直接for循环效率最高,其次是迭代器和 ForEach操作。
作为语法糖,其实 ForEach 编译成 字节码之后,使用的是迭代器实现的,反编译后,testForEach方法如下:
public static void testForEach(List list) {
for (Iterator iterator = list.iterator(); iterator.hasNext();) {
Object t = iterator.next();
Object obj = t;
}
}
可以看到,只比迭代器遍历多了生成中间变量这一步,因为性能也略微下降了一些。
最后
以上就是落寞向日葵为你收集整理的【Java程序优化】- 深度剖析 List 性能分析的全部内容,希望文章能够帮你解决【Java程序优化】- 深度剖析 List 性能分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复