【java集合类】ArrayList和LinkedList源码分析(jdk1.8)

被刻印的时光 ゝ 提交于 2021-02-06 09:08:23

前言:

  ArrayList底层是依靠数组实现的,而LinkedList的实现是含前驱后继节点的双向列表。平时刷题时会经常使用到这两个集合类,这两者的区别在我眼中主要是ArrayList读取节点平均时间复杂度是O(1)级别的,插入删除节点是O(n);LinkedList读取节点时间复杂度是O(n),插入节点是O(1)。

  本文记录我对jdk1.8下的ArrayList和LinkedList源码中主要内容的学习。

1、ArrayList

1.1 主要成员变量

 1     //默认容量
 2     private static final int DEFAULT_CAPACITY = 10;
 3     //空的数组
 4     private static final Object[] EMPTY_ELEMENTDATA = {};
 5 
 6     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 7     //数据数组
 8     transient Object[] elementData; // non-private to simplify nested class access
 9     //当前大小
10     private int size;

  主要成员变量如上,最重要的就是size和elementData,其中elementData的修饰transient一开始很令我费解,查阅资料后豁然开朗,transient是为了序列化ArrayList时不用Java自带的序列化机制,而用ArrayList定义的两个方法(writeObject、readObject),实现自己可控制的序列化操作,防止数组中大量NULL元素被序列化。

1.2 主要方法

1.2.1 构造方法

  构造方法源码其实很简单,不过在此提及是为了给后面扩容引出一个思考。

 1     public ArrayList(int initialCapacity) {
 2         if (initialCapacity > 0) {
 3             this.elementData = new Object[initialCapacity];
 4         } else if (initialCapacity == 0) {
 5             this.elementData = EMPTY_ELEMENTDATA;
 6         } else {
 7             throw new IllegalArgumentException("Illegal Capacity: "+
 8                                                initialCapacity);
 9         }
10     }
11 
12    
13     public ArrayList() {
14         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
15     }

  源码如上,一个不带参数的构造器,以及带容量参数的构造器。

1.2.2 add方法

 1     public boolean add(E e) {
 2         ensureCapacityInternal(size + 1);  // Increments modCount!!
 3         elementData[size++] = e;//加到末尾
 4         return true;
 5     }
 6 
 7     private void ensureCapacityInternal(int minCapacity) {
 8         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 9             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
10         }
11 
12         ensureExplicitCapacity(minCapacity);
13     }
14 
15     //判断是否需要扩容
16     private void ensureExplicitCapacity(int minCapacity) {
17         modCount++;
18 
19         // overflow-conscious code
20         if (minCapacity - elementData.length > 0)
21             grow(minCapacity);
22     }

  add方法中先用ensureCapacityInternal方法,首先判断是否位第一次add,也就是初始化。如果数组位空,那么DEFAULT_CAPACITY就是为10。然后判断是否需要扩容,如果原size+1比数组的length大就需要扩容。(扩容)后把要加的元素加到末尾即可。

1.2.3 扩容方法(grow)

 1     private void grow(int minCapacity) {
 2         // overflow-conscious code
 3         int oldCapacity = elementData.length;
 4         int newCapacity = oldCapacity + (oldCapacity >> 1);
 5         if (newCapacity - minCapacity < 0)
 6             newCapacity = minCapacity;
 7         if (newCapacity - MAX_ARRAY_SIZE > 0)
 8             newCapacity = hugeCapacity(minCapacity);
 9         // minCapacity is usually close to size, so this is a win:
10         elementData = Arrays.copyOf(elementData, newCapacity);
11     }

  扩容方法如上,hugeCapacity判断minCapacity是否大于ArrayList上限,如果大于就返回ArrayList的容量上限。用Arrays.copyof新生成一个数组,而newCapacity = oldCapacity + (oldCapacity >> 1)则是将容量变为原来的1.5倍。

  因为ArrayList默认初始容量为10,每次扩容将容量变为1.5倍,而如果使用ArrayList时要一次性add100个元素,则会频繁用调用扩容方法,因此可以在初始化ArrayList时使用带参的构造函数,定一个合适的容量值。

1.2.4 remove方法

 1     public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // clear to let GC do its work
12 
13         return oldValue;
14     }
15 
16     public boolean remove(Object o) {
17         if (o == null) {
18             for (int index = 0; index < size; index++)
19                 if (elementData[index] == null) {
20                     fastRemove(index);
21                     return true;
22                 }
23         } else {
24             for (int index = 0; index < size; index++)
25                 if (o.equals(elementData[index])) {
26                     fastRemove(index);
27                     return true;
28                 }
29         }
30         return false;
31     }

  remove方法主要有两种,一种是根据下标remove,另一种是根据传入的元素匹配删除第一个遇到的该元素,值得一提的是可以删除null元素(总感觉怪怪的)。

2、LinkedList

  LinkedList是一个双向链表,可以当(双端)队列用。

2.1 主要成员变量

 1     transient int size = 0;
 2     
 3     //头节点
 4     transient Node<E> first;
 5 
 6     //尾节点
 7     transient Node<E> last;
 8 
 9     //Node节点
10     private static class Node<E> {
11         E item;
12         Node<E> next;//前驱
13         Node<E> prev;//后继
14 
15         Node(Node<E> prev, E element, Node<E> next) {
16             this.item = element;
17             this.next = next;
18             this.prev = prev;
19         }
20     }

  带首尾的双向列表,加一个size变量记录当前节点数量,transient修饰和ArrayList中修饰数组的原因是一样的,同样实现了writeObject和readObject,自己实现把size和每一个节点都序列化和反序列化了。

2.2 主要方法

2.2.1 add方法

 1     //add方法添加元素到末尾
 2     public boolean add(E e) {
 3         linkLast(e);
 4         return true;
 5     }
 6     
 7     //添加元素至末尾
 8     void linkLast(E e) {
 9         final Node<E> l = last;
10         final Node<E> newNode = new Node<>(l, e, null);//新建元素,把前驱节点置为原来的last节点
11         last = newNode;
12         if (l == null)//如果尾节点是空(说明头节点也是空的),就把头节点设置成新节点
13             first = newNode;
14         else//原来尾节点的后继设置成新节点
15             l.next = newNode;
16         size++;
17         modCount++;
18     }

  一种add就是上面代码的加到末尾,分析都在注释中了。另一种则是添加到指定index,add的平均时间复杂度为O(n)。

 1     public void add(int index, E element) {
 2         checkPositionIndex(index);
 3 
 4         if (index == size)
 5             linkLast(element);//index是最后一个就直接插到最后
 6         else
 7             linkBefore(element, node(index));
 8     }
 9 
10     //将节点插入到目标节点前面
11     void linkBefore(E e, Node<E> succ) {
12         // assert succ != null;
13         final Node<E> pred = succ.prev;
14         final Node<E> newNode = new Node<>(pred, e, succ);//将插入节点的前驱设置成目标节点的前驱
15         succ.prev = newNode;
16         if (pred == null)//同linkLast中设置后驱节点为目标节点
17             first = newNode;
18         else
19             pred.next = newNode;
20         size++;
21         modCount++;
22     }

2.2.2 remove方法

  remove方法和add方法类似,由于是双端队列,因此需要改变删除节点的前驱和后继节点的后继和前驱。在此不再展开描述。

2.2.3 get方法

  get方法在此不贴源码了,由于是双端队列,因此如果查找的下标大于size的一半,就从后面往前遍历,虽然时间复杂度还是o(n)级别的,不过也算是一个小优化吧。

 

  本篇简略的对jdk1.8下的ArrayList和LinkedList源码实现进行了分析,期间被几个命名奇怪的方法勾引走了,比如ArrayList的trimToSize,可以将数组多余的(大于size)的部分“删掉”。也学到了不少(emmm,好像没有特别多)东西。本篇博客算是对学习过程的一个记录吧。(才不会说是好久没更新博客了要懒死了QAQ)。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!