LinkedList、ArrayList分析

List 是重要的数据结构之一。最常用的的便是: ArrayList、Vector 和 LinkedList 三种了,他们类图如下图所示:


20140909233314543.png

由上图可知,这三种 List 都实现了 Collection 和 List 接口。
这三种不同的实现中,ArrayList 和 Vector 使用了数组实现,封装了对内部数组的操作。它们俩唯一的区别是 ArrayList 没有任何一个方法是同步的,而 Vector 则是做了线程同步。我们之后均以 ArrayList 为例进行讲解。
LinkList 使用了双向链表数据结构,并且维护了 first 和 last 两个成员变量来指示链表的头和尾。这和 ArrayList 是截然不同的实现技术,也决定了它们适用于不同的场景中。
LinkedList 是由一系列表项连接而成的。一个表项总是分为三个部分,分别是:元素内容,前驱表项 和 后驱表项,如下图所示:


20140909234443750.png

在 JDK 的视线中,不管 LinkedList 是否为空,链表中总是有一个** header** 表项,它即表示链表的开始,也表示链表的结尾。

下面以增加和删除为例,比较 ArrayList 和 LinkList 的不同之处。

1、增加元素到列表尾端

在 ArrayList 中增加元素到队列端尾的代码如下:

public boolean add(E e) {  
        ensureCapacityInternal(size + 1);  // 确保内部数组有足够的空间  
        elementData[size++] = e;  
        return true;  
    }  

ArrayList 中 add() 方法的性能取决于 ensureCapacity() 方法。ensureCapacity() 的实现如下:

/** 
 * 分配的最大内存。 
 * 有些虚拟机会为数组保留了一些头部内容,试图分配更多的空间会引起内存溢出 
    */  
   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 分析运行结果为:


20140913004433453.jpg

可以看到,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();  
    }  
}  

运行以上代码,执行时间如下:


20140913235017296.jpg

可以看出,两者性能有天差地壤的差别。
然后每次都插入中间位置:

list.add(list.size()>>1, obj);  

性能结果如下:


2.jpg

可以看到,此时 LinkedList 的性能反而满了好几倍。
我们再插入末尾位置:

list.add(list.size(), obj);  

性能结果如下:


20140914002640922.jpg

我们可以看到,依然是 LinkedList 的性能较慢。

因此通过对比以上三种情况下的插入操作,我们得到如下结论:当插入的位置靠前的时候,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倍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容