ArrayList 与 LinkedList 比较(实现方式)

 

        欢迎访问我的个人博客

         面试的时候,经常会有面试官提问Collection接口下面实现类的区别,以及各自适用场景,不少人都能答出来:ArrayList基于数组实现,善于随机访问和遍历集合,做随机位置插入和删除性能较差,而ArrayList基于链表结构实现(双向链表),在做随机位置插入时性能较好于ArrayList,在做遍历和随机访问时性能较差。但很多人不知道到底为什么基于数组实现随机访问就快?任意位置插入性能就差?而基于链表实现的正好相反呢?

         要弄清楚这个问题,必须要清楚其实现方式,最好的办法就是去看源码,这是一种好的

学习习惯。

从上面图可以看到List接口下的所有实现类和父接口,其中当然包括本文介绍的主角儿ArrayList,LinkedList,既然是实现自同一接口,那么他们的规范自然是一样的。

         如上图,因为源码注释太多,看着烦躁,这里我用Ctrl+o可以看到List接口下面的所有方法,接下来,我们来看看他们各自的实现的代码吧!

         首先我们看ArrayList,在类中有个Object[] elementData,可以想到他是用来存放Arraylist中的数据的。

那么,它是怎样对数据进行增删改查操作的?

1
2
3
4
5
public  boolean  add(E e) {
         ensureCapacityInternal(size +  1 );   // Increments modCount!!
         elementData[size++] = e;
         return  true ;
}

 

这是add方法,主要是将e赋值给数组,将数组长度加一,ensureCapacityInternal里面主要是做一些逻辑判断,请看grow方法

1
2
3
4
5
6
7
8
9
10
11
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(elementData, newCapacity);这行代码非常关键,他是影响ArrayList插入性能的主要因素,当数组下表超过数组长度时会调用这个方法,调用Arrays.copyOf的方法时会重新copy构建一个新的数组,会消耗大量的资源,导致性能变的很差。

下面看看随机插入add(int index, E element)

1
2
3
4
5
6
7
8
9
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++;
}

 

此处更是不消多说,就是copy copy copy,大量的数组移动,导致性能底下。remove方法与add方法实现类似.

再来看看随机访问和遍历,get方法非常简单

1
2
3
4
public  E get( int  index) {
         rangeCheck(index);
         return  elementData(index);
}

 

直接指向数组下标位置的元素返回,当然是最快的,遍历亦是如此。

再来看看LinkedList的内部实现吧

 

内部有两个Node对象,一个指向集合第一个,一个指向最后一个,Node内部代码如下

1
2
3
4
5
6
7
8
9
10
11
private  static  class  Node<E> {
         E item;
         Node<E> next;
         Node<E> prev;
  
         Node(Node<E> prev, E element, Node<E> next) {
             this .item = element;
             this .next = next;
             this .prev = prev;
         }
}

 

每一个Node都有nextprev两个对象,相互关联这就形成了一条链子,所以这个叫做链表结构(双向),用网上的一个图片。

 

再来看看他们实现addremove的方法吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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会调用linkLast,linkdLast方法内部会新建一个Node对象,将要存储的值放入node中,同时把当前的node绑定到集合最末端,时间复杂度为O(n).

再看指定位置的插入,add(int index, E element)方法。

1
2
3
4
5
6
7
8
public  void  add( int  index, E element) {
         checkPositionIndex(index);
  
         if  (index == size)
             linkLast(element);
         else
             linkBefore(element, node(index));
}

 

当需要插入的位置为集合大小时,可以直接插入集合尾部,否则,则需要找到指定位置的下标然后修改他们nextprev的指向,这里会有一个遍历,JDK对其做了优化,不同位置从两端遍历,这样让遍历的查找时间降到最低。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
         }
  }

 

removeadd逻辑类似。

 

总结一下,ArrayList得其天赋是数组,数组在随机访问时的速度是与生俱来的,而其软肋就是数组是固定的,大小固定,位置固定,所以你做新增(特别是中间段的新增或者是长度已达目前初始大小时),删除会频繁移动数组,导致性能底下。

而在LinkedList里面,由于他使用双向链表结构实现,那么新增和删除,只需要修改相应的指向就行了,在中间位置插入会比在末尾插入性能要差点,在随机访问时,由于需要遍历,所以速度逊于ArrayList,在做遍历的时候也是一样,使用迭代器可以缓解这个问题。

         值得一提的时,假设我需要一个长度为固定数量S的集合,默认向里面add数据,给ArrayList赋予初始初始容量S,那么ArrayList插入的性能会优于LinkedList,请看测试代码和运行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  static  void  main(String[] args) {
         int  size= 10000000 ;
         testArraylistAdd(size);
         testLikedlistAdd(size);
     }
  
     static  void  testArraylistAdd( int  size) {
         long  t1 = System.currentTimeMillis();
         List<Integer> list =  new  ArrayList<Integer>(size);
         for  ( int  i =  0 ; i < size; i++) {
             list.add(i);
         }
         long  t2 = System.currentTimeMillis();
         System.out.println( "测试"  + size +  "条数据,插入ArrayList所花费时间 为:"  + (t2 - t1));
     }
  
     static  void  testLikedlistAdd( int  size) {
         long  t1 = System.currentTimeMillis();
         List<Integer> list =  new  LinkedList<Integer>();
         for  ( int  i =  0 ; i < size; i++) {
             list.add(i);
         }
         long  t2 = System.currentTimeMillis();
         System.out.println( "测试"  + size +  "条数据,插入LinkedList所花费时间 为:"  + (t2 - t1));
     }

 

测试10000000条数据,插入ArrayList所花费时间 为:78

测试10000000条数据,插入LinkedList所花费时间 为:4738

主要是因为初始时指定了数组长度,插入时不需要频繁移动数组,所以速度才会优于LinkedList

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值