ArrayList VS LinkedList,最后一战

开发 前端
序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。

[[428384]]

 01、ArrayList 是如何实现的?

ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。

底层是基于数组实现的,并且实现了动态扩容

  1. public class ArrayList<E> extends AbstractList<E> 
  2.         implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
  3.     private static final int DEFAULT_CAPACITY = 10; 
  4.     transient Object[] elementData; 
  5.     private int size

ArrayList 还实现了 RandomAccess 接口,这是一个标记接口:

  1. public interface RandomAccess { 

内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。

  1. public E get(int index) { 
  2.     Objects.checkIndex(indexsize); 
  3.     return elementData(index); 
  4. E elementData(int index) { 
  5.     return (E) elementData[index]; 

ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 clone() 方法。

  1. public Object clone() { 
  2.     try { 
  3.         ArrayList<?> v = (ArrayList<?>) super.clone(); 
  4.         v.elementData = Arrays.copyOf(elementData, size); 
  5.         v.modCount = 0; 
  6.         return v; 
  7.     } catch (CloneNotSupportedException e) { 
  8.         // this shouldn't happen, since we are Cloneable 
  9.         throw new InternalError(e); 
  10.     } 

ArrayList 还实现了 Serializable 接口,同样是一个标记接口:

  1. public interface Serializable { 

内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。

眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。

这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢?

这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。

ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 s == elementData.length,就按照原来数组的 1.5 倍(oldCapacity >> 1)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 Arrays.copyOf(elementData, newCapacity)。

 

  1. private void add(E e, Object[] elementData, int s) { 
  2.     if (s == elementData.length) 
  3.         elementData = grow(); 
  4.     elementData[s] = e; 
  5.     size = s + 1; 
  6.  
  7. private Object[] grow() { 
  8.     return grow(size + 1); 
  9.  
  10. private Object[] grow(int minCapacity) { 
  11.     int oldCapacity = elementData.length; 
  12.     if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
  13.         int newCapacity = ArraysSupport.newLength(oldCapacity, 
  14.                 minCapacity - oldCapacity, /* minimum growth */ 
  15.                 oldCapacity >> 1           /* preferred growth */); 
  16.         return elementData = Arrays.copyOf(elementData, newCapacity); 
  17.     } else { 
  18.         return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; 
  19.     } 

 

动态扩容意味着什么?大家伙想一下。嗯,还是我来告诉大家答案吧,有点迫不及待。

意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。

比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧?

序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。

于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。

  1. private void writeObject(java.io.ObjectOutputStream s) 
  2.         throws java.io.IOException { 
  3.     // Write out element countand any hidden stuff 
  4.     int expectedModCount = modCount; 
  5.     s.defaultWriteObject(); 
  6.  
  7.     // Write out size as capacity for behavioral compatibility with clone() 
  8.     s.writeInt(size); 
  9.  
  10.     // Write out all elements in the proper order
  11.     for (int i=0; i<size; i++) { 
  12.         s.writeObject(elementData[i]); 
  13.     } 
  14.  
  15.     if (modCount != expectedModCount) { 
  16.         throw new ConcurrentModificationException(); 
  17.     } 

从 writeObject 方法的源码中可以看得出,它使用了 ArrayList 的实际大小 size 而不是数组的长度(elementData.length)来作为元素的上限进行序列化。

此处应该有掌声啊!不是为我,为 Java 源码的作者们,他们真的是太厉害了,可以用两个词来形容他们——殚精竭虑、精益求精。

02、LinkedList 是如何实现的?

LinkedList 是一个继承自 AbstractSequentialList 的双向链表,因此它也可以被当作堆栈、队列或双端队列进行操作。

  1. public class LinkedList<E> 
  2.     extends AbstractSequentialList<E> 
  3.     implements List<E>, Deque<E>, Cloneable, java.io.Serializable 
  4.     transient int size = 0; 
  5.     transient Node<E> first
  6.     transient Node<E> last

LinkedList 内部定义了一个 Node 节点,它包含 3 个部分:元素内容 item,前引用 prev 和后引用 next。代码如下所示:

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

LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。

LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。眼睛雪亮的小伙伴可能又注意到了,LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化?

答案是 LinkedList 想按照自己的方式序列化,来看它自己实现的 writeObject() 方法:

  1. private void writeObject(java.io.ObjectOutputStream s) 
  2.         throws java.io.IOException { 
  3.     // Write out any hidden serialization magic 
  4.     s.defaultWriteObject(); 
  5.  
  6.     // Write out size 
  7.     s.writeInt(size); 
  8.  
  9.     // Write out all elements in the proper order
  10.     for (LinkedList.Node<E> x = first; x != null; x = x.next
  11.         s.writeObject(x.item); 

发现没?LinkedList 在序列化的时候只保留了元素的内容 item,并没有保留元素的前后引用。这样就节省了不少内存空间,对吧?

那有些小伙伴可能就疑惑了,只保留元素内容,不保留前后引用,那反序列化的时候怎么办?

  1. private void readObject(java.io.ObjectInputStream s) 
  2.         throws java.io.IOException, ClassNotFoundException { 
  3.     // Read in any hidden serialization magic 
  4.     s.defaultReadObject(); 
  5.  
  6.     // Read in size 
  7.     int size = s.readInt(); 
  8.  
  9.     // Read in all elements in the proper order
  10.     for (int i = 0; i < size; i++) 
  11.         linkLast((E)s.readObject()); 
  12.  
  13. void linkLast(E e) { 
  14.     final LinkedList.Node<E> l = last
  15.     final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null); 
  16.     last = newNode; 
  17.     if (l == null
  18.         first = newNode; 
  19.     else 
  20.         l.next = newNode; 
  21.     size++; 
  22.     modCount++; 

注意 for 循环中的 linkLast() 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧?

和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。

03、ArrayList 和 LinkedList 新增元素时究竟谁快?

前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快?

1)ArrayList

ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。

添加到数组末尾的源码:

  1. public boolean add(E e) { 
  2.     modCount++; 
  3.     add(e, elementData, size); 
  4.     return true
  5.  
  6. private void add(E e, Object[] elementData, int s) { 
  7.     if (s == elementData.length) 
  8.         elementData = grow(); 
  9.     elementData[s] = e; 
  10.     size = s + 1; 

很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。

插入到指定位置的源码:

  1. public void add(int index, E element) { 
  2.     rangeCheckForAdd(index); 
  3.     modCount++; 
  4.     final int s; 
  5.     Object[] elementData; 
  6.     if ((s = size) == (elementData = this.elementData).length) 
  7.         elementData = grow(); 
  8.     System.arraycopy(elementData, index
  9.             elementData, index + 1, 
  10.             s - index); 
  11.     elementData[index] = element; 
  12.     size = s + 1; 

先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。这种情况是非常伤的,性能会比较差。

2)LinkedList

LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。

添加到队尾的源码:

  1. public boolean add(E e) { 
  2.     linkLast(e); 
  3.     return true
  4. void linkLast(E e) { 
  5.     final LinkedList.Node<E> l = last
  6.     final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null); 
  7.     last = newNode; 
  8.     if (l == null
  9.         first = newNode; 
  10.     else 
  11.         l.next = newNode; 
  12.     size++; 
  13.     modCount++; 

先将队尾的节点 last 存放到临时变量 l 中(不是说不建议使用 I 作为变量名吗?Java 的作者们明知故犯啊),然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。

插入到指定位置的源码:

  1. public void add(int index, E element) { 
  2.     checkPositionIndex(index); 
  3.  
  4.     if (index == size
  5.         linkLast(element); 
  6.     else 
  7.         linkBefore(element, node(index)); 
  8. LinkedList.Node<E> node(int index) { 
  9.     // assert isElementIndex(index); 
  10.  
  11.     if (index < (size >> 1)) { 
  12.         LinkedList.Node<E> x = first
  13.         for (int i = 0; i < index; i++) 
  14.             x = x.next
  15.         return x; 
  16.     } else { 
  17.         LinkedList.Node<E> x = last
  18.         for (int i = size - 1; i > index; i--) 
  19.             x = x.prev; 
  20.         return x; 
  21.     } 
  22. void linkBefore(E e, LinkedList.Node<E> succ) { 
  23.     // assert succ != null
  24.     final LinkedList.Node<E> pred = succ.prev; 
  25.     final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ); 
  26.     succ.prev = newNode; 
  27.     if (pred == null
  28.         first = newNode; 
  29.     else 
  30.         pred.next = newNode; 
  31.     size++; 
  32.     modCount++; 

先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 linkBefore() 方法。

在执行 linkBefore() 方法之前,会调用 node() 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。

找到指定位置上的元素(succ)之后,就开始执行 linkBefore() 方法了,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。

经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!”

当两者的起始长度是一样的情况下:

  • 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。
  1. public class ArrayListTest { 
  2.     public static void addFromHeaderTest(int num) { 
  3.         ArrayList<String> list = new ArrayList<String>(num); 
  4.         int i = 0; 
  5.  
  6.         long timeStart = System.currentTimeMillis(); 
  7.  
  8.         while (i < num) { 
  9.             list.add(0, i + "沉默王二"); 
  10.             i++; 
  11.         } 
  12.         long timeEnd = System.currentTimeMillis(); 
  13.  
  14.         System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); 
  15.     } 
  16.  
  17. /** 
  18.  * @author 微信搜「沉默王二」,回复关键字 PDF 
  19.  */ 
  20. public class LinkedListTest { 
  21.     public static void addFromHeaderTest(int num) { 
  22.         LinkedList<String> list = new LinkedList<String>(); 
  23.         int i = 0; 
  24.         long timeStart = System.currentTimeMillis(); 
  25.         while (i < num) { 
  26.             list.addFirst(i + "沉默王二"); 
  27.             i++; 
  28.         } 
  29.         long timeEnd = System.currentTimeMillis(); 
  30.  
  31.         System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); 
  32.     } 

num 为 10000,代码实测后的时间如下所示:

  1. ArrayList从集合头部位置新增元素花费的时间595 
  2. LinkedList从集合头部位置新增元素花费的时间15 

ArrayList 花费的时间比 LinkedList 要多很多。

  • 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。
  1. public class ArrayListTest { 
  2.     public static void addFromMidTest(int num) { 
  3.         ArrayList<String> list = new ArrayList<String>(num); 
  4.         int i = 0; 
  5.  
  6.         long timeStart = System.currentTimeMillis(); 
  7.         while (i < num) { 
  8.             int temp = list.size(); 
  9.             list.add(temp / 2 + "沉默王二"); 
  10.             i++; 
  11.         } 
  12.         long timeEnd = System.currentTimeMillis(); 
  13.  
  14.         System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); 
  15.     } 
  16.  
  17. public class LinkedListTest { 
  18.     public static void addFromMidTest(int num) { 
  19.         LinkedList<String> list = new LinkedList<String>(); 
  20.         int i = 0; 
  21.         long timeStart = System.currentTimeMillis(); 
  22.         while (i < num) { 
  23.             int temp = list.size(); 
  24.             list.add(temp / 2, i + "沉默王二"); 
  25.             i++; 
  26.         } 
  27.         long timeEnd = System.currentTimeMillis(); 
  28.  
  29.         System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); 
  30.     } 

num 为 10000,代码实测后的时间如下所示:

  1. ArrayList从集合中间位置新增元素花费的时间1 
  2.  
  3. LinkedList从集合中间位置新增元素花费的时间101 

ArrayList 花费的时间比 LinkedList 要少很多很多。

  • 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。
  1. public class ArrayListTest { 
  2.     public static void addFromTailTest(int num) { 
  3.         ArrayList<String> list = new ArrayList<String>(num); 
  4.         int i = 0; 
  5.  
  6.         long timeStart = System.currentTimeMillis(); 
  7.  
  8.         while (i < num) { 
  9.             list.add(i + "沉默王二"); 
  10.             i++; 
  11.         } 
  12.  
  13.         long timeEnd = System.currentTimeMillis(); 
  14.  
  15.         System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); 
  16.     } 
  17.  
  18. public class LinkedListTest { 
  19.     public static void addFromTailTest(int num) { 
  20.         LinkedList<String> list = new LinkedList<String>(); 
  21.         int i = 0; 
  22.         long timeStart = System.currentTimeMillis(); 
  23.         while (i < num) { 
  24.             list.add(i + "沉默王二"); 
  25.             i++; 
  26.         } 
  27.         long timeEnd = System.currentTimeMillis(); 
  28.  
  29.         System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); 
  30.     } 

num 为 10000,代码实测后的时间如下所示:

  1. ArrayList从集合尾部位置新增元素花费的时间69 
  2. LinkedList从集合尾部位置新增元素花费的时间193 

ArrayList 花费的时间比 LinkedList 要少一些。

这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。

当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。

04、ArrayList 和 LinkedList 删除元素时究竟谁快?

1)ArrayList

ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(remove(Object)),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(remove(int))。

  1. public boolean remove(Object o) { 
  2.     final Object[] es = elementData; 
  3.     final int size = this.size
  4.     int i = 0; 
  5.     found: { 
  6.         if (o == null) { 
  7.             for (; i < size; i++) 
  8.                 if (es[i] == null
  9.                     break found; 
  10.         } else { 
  11.             for (; i < size; i++) 
  12.                 if (o.equals(es[i])) 
  13.                     break found; 
  14.         } 
  15.         return false
  16.     } 
  17.     fastRemove(es, i); 
  18.     return true
  19. public E remove(int index) { 
  20.     Objects.checkIndex(indexsize); 
  21.     final Object[] es = elementData; 
  22.  
  23.     @SuppressWarnings("unchecked") E oldValue = (E) es[index]; 
  24.     fastRemove(es, index); 
  25.  
  26.     return oldValue; 

但从本质上讲,都是一样的,因为它们最后调用的都是 fastRemove(Object, int) 方法。

  1. private void fastRemove(Object[] es, int i) { 
  2.     modCount++; 
  3.     final int newSize; 
  4.     if ((newSize = size - 1) > i) 
  5.         System.arraycopy(es, i + 1, es, i, newSize - i); 
  6.     es[size = newSize] = null

从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。

2)LinkedList

LinkedList 删除元素的时候,有四种常用的方式:

  • remove(int),删除指定位置上的元素
  1. public E remove(int index) { 
  2.     checkElementIndex(index); 
  3.     return unlink(node(index)); 

先检查索引,再调用 node(int) 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 unlink(Node) 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:

  1. E unlink(Node<E> x) { 
  2.     // assert x != null
  3.     final E element = x.item; 
  4.     final Node<E> next = x.next
  5.     final Node<E> prev = x.prev; 
  6.  
  7.     if (prev == null) { 
  8.         first = next
  9.     } else { 
  10.         prev.next = next
  11.         x.prev = null
  12.     } 
  13.  
  14.     if (next == null) { 
  15.         last = prev; 
  16.     } else { 
  17.         next.prev = prev; 
  18.         x.next = null
  19.     } 
  20.  
  21.     x.item = null
  22.     size--; 
  23.     modCount++; 
  24.     return element; 
  • remove(Object),直接删除元素
  1. public boolean remove(Object o) { 
  2.     if (o == null) { 
  3.         for (LinkedList.Node<E> x = first; x != null; x = x.next) { 
  4.             if (x.item == null) { 
  5.                 unlink(x); 
  6.                 return true
  7.             } 
  8.         } 
  9.     } else { 
  10.         for (LinkedList.Node<E> x = first; x != null; x = x.next) { 
  11.             if (o.equals(x.item)) { 
  12.                 unlink(x); 
  13.                 return true
  14.             } 
  15.         } 
  16.     } 
  17.     return false

也是先前后半段遍历,找到要删除的元素后调用 unlink(Node)。

  • removeFirst(),删除第一个节点
  1. public E removeFirst() { 
  2.     final LinkedList.Node<E> f = first
  3.     if (f == null
  4.         throw new NoSuchElementException(); 
  5.     return unlinkFirst(f); 
  6. private E unlinkFirst(LinkedList.Node<E> f) { 
  7.     // assert f == first && f != null
  8.     final E element = f.item; 
  9.     final LinkedList.Node<E> next = f.next
  10.     f.item = null
  11.     f.next = null; // help GC 
  12.     first = next
  13.     if (next == null
  14.         last = null
  15.     else 
  16.         next.prev = null
  17.     size--; 
  18.     modCount++; 
  19.     return element; 

删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。

  • removeLast(),删除最后一个节点

删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。

可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。

这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致:

  • 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多;
  • 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多;
  • 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。

我本地的统计结果如下所示,小伙伴们可以作为参考:

  1. ArrayList从集合头部位置删除元素花费的时间380 
  2. LinkedList从集合头部位置删除元素花费的时间4 
  3. ArrayList从集合中间位置删除元素花费的时间381 
  4. LinkedList从集合中间位置删除元素花费的时间5922 
  5. ArrayList从集合尾部位置删除元素花费的时间8 
  6. LinkedList从集合尾部位置删除元素花费的时间12 

05、ArrayList 和 LinkedList 遍历元素时究竟谁快?

1)ArrayList

遍历 ArrayList 找到某个元素的话,通常有两种形式:

  • get(int),根据索引找元素
  1. public E get(int index) { 
  2.     Objects.checkIndex(indexsize); 
  3.     return elementData(index); 

由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。

  • indexOf(Object),根据元素找索引
  1. public int indexOf(Object o) { 
  2.     return indexOfRange(o, 0, size); 
  3.  
  4. int indexOfRange(Object o, int start, int end) { 
  5.     Object[] es = elementData; 
  6.     if (o == null) { 
  7.         for (int i = start; i < end; i++) { 
  8.             if (es[i] == null) { 
  9.                 return i; 
  10.             } 
  11.         } 
  12.     } else { 
  13.         for (int i = start; i < end; i++) { 
  14.             if (o.equals(es[i])) { 
  15.                 return i; 
  16.             } 
  17.         } 
  18.     } 
  19.     return -1; 

根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。

2)LinkedList

遍历 LinkedList 找到某个元素的话,通常也有两种形式:

  • get(int),找指定位置上的元素
  1. public E get(int index) { 
  2.     checkElementIndex(index); 
  3.     return node(index).item; 

既然需要调用 node(int) 方法,就意味着需要前后半段遍历了。

  • indexOf(Object),找元素所在的位置
  1. public int indexOf(Object o) { 
  2.     int index = 0; 
  3.     if (o == null) { 
  4.         for (LinkedList.Node<E> x = first; x != null; x = x.next) { 
  5.             if (x.item == null
  6.                 return index
  7.             index++; 
  8.         } 
  9.     } else { 
  10.         for (LinkedList.Node<E> x = first; x != null; x = x.next) { 
  11.             if (o.equals(x.item)) 
  12.                 return index
  13.             index++; 
  14.         } 
  15.     } 
  16.     return -1; 

需要遍历整个链表,和 ArrayList 的 indexOf() 类似。

那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用迭代器(Iterator)。

如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 node(int) 方法进行前后半段的遍历。

  1. LinkedList.Node<E> node(int index) { 
  2.     // assert isElementIndex(index); 
  3.  
  4.     if (index < (size >> 1)) { 
  5.         LinkedList.Node<E> x = first
  6.         for (int i = 0; i < index; i++) 
  7.             x = x.next
  8.         return x; 
  9.     } else { 
  10.         LinkedList.Node<E> x = last
  11.         for (int i = size - 1; i > index; i--) 
  12.             x = x.prev; 
  13.         return x; 
  14.     } 

那如果使用的是迭代器呢?

  1. LinkedList<String> list = new LinkedList<String>(); 
  2. for (Iterator<String> it = list.iterator(); it.hasNext();) { 
  3.     it.next(); 

迭代器只会调用一次 node(int) 方法,在执行 list.iterator() 的时候:先调用 AbstractSequentialList 类的 iterator() 方法,再调用 AbstractList 类的 listIterator() 方法,再调用 LinkedList 类的 listIterator(int) 方法,如下图所示。

最后返回的是 LinkedList 类的内部私有类 ListItr 对象:

  1. public ListIterator<E> listIterator(int index) { 
  2.     checkPositionIndex(index); 
  3.     return new LinkedList.ListItr(index); 
  4.  
  5. private class ListItr implements ListIterator<E> { 
  6.     private LinkedList.Node<E> lastReturned; 
  7.     private LinkedList.Node<E> next
  8.     private int nextIndex; 
  9.     private int expectedModCount = modCount; 
  10.  
  11.     ListItr(int index) { 
  12.         // assert isPositionIndex(index); 
  13.         next = (index == size) ? null : node(index); 
  14.         nextIndex = index
  15.     } 
  16.  
  17.     public boolean hasNext() { 
  18.         return nextIndex < size
  19.     } 
  20.  
  21.     public E next() { 
  22.         checkForComodification(); 
  23.         if (!hasNext()) 
  24.             throw new NoSuchElementException(); 
  25.  
  26.         lastReturned = next
  27.         next = next.next
  28.         nextIndex++; 
  29.         return lastReturned.item; 
  30.     } 

执行 ListItr 的构造方法时调用了一次 node(int) 方法,返回第一个节点。在此之后,迭代器就执行 hasNext() 判断有没有下一个,执行 next() 方法下一个节点。

由此,可以得出这样的结论:遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。

也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。

06、总结

花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时了。

本文转载自微信公众号「沉默王二」,可以通过以下二维码关注。转载本文请联系沉默王二公众号。

 

责任编辑:武晓燕 来源: 沉默王二
相关推荐

2013-09-13 09:19:36

微软IBM惠普

2015-04-20 15:55:52

2015-05-18 10:53:33

2022-03-12 15:03:59

存储闪存硬盘数据中心

2015-09-28 17:20:12

智慧

2014-08-25 15:19:11

MIUI 6

2020-05-15 10:52:41

大数据人工智能技术

2024-06-20 07:38:44

2019-12-30 09:14:54

张一鸣互联网高管

2012-02-27 10:17:25

2011-05-26 14:49:53

ArrayListLinkedList

2016-12-16 10:55:19

2010-10-09 09:19:30

2024-10-05 00:00:15

ArrayList性能Java

2018-11-19 12:27:09

2023-05-16 07:39:15

ArrayList磁盘IO

2021-11-03 16:09:40

Java泛型代码

2013-08-07 14:19:39

移动互联网BAT三巨头移动市场

2011-10-26 09:29:40

诺基亚诺基亚世界大会WP

2011-08-16 10:53:02

点赞
收藏

51CTO技术栈公众号