Java集合源码分析<二>:LinkedList的源码分析
一、底层结构原理
LinkedList底层基于双向链表实现,相比于单向链表,双向链表可以查找上个节点,实现双向遍历,更加灵活,同时占用资源也更多。
上图描述了LinkedList的底层结构,LinkedList内部成员变量极少,只有first、last、size,first是链表头节点,last是尾结点,size是元素数量,LinkedList链表的每个节点为Node<E>类,源码如下:
// LinkedList.java 87行
transient int size = 0; transient Node<E> first; // 头节点 transient Node<E> last; // 尾节点
// LinkedList.java 970行
private static class Node<E> { E item; // 元素值 Node<E> next; // 下一个节点, 尾节点为null Node<E> prev; // 上一个节点, 头节点为null Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
Node中的next和prev分别指向上一个节点和下一个节点,这是链表实现的基础,后面LinkedList的增删改查都会跟随这两个变量进行。
二、添加元素
LinkedList基于链表实现,长度跟随元素变化,所以也不会有类型ArrayList的扩容机制和长度限制,理论上在JVM内存允许范围可无限增长,LinkedList的元素添加有四个方法,add(E e),add(int index, E element),addAll(Collection<? extends E> c),addAll(int index, Collection<? extends E> c),对于指定位置的插入(add(int index, E element)方法),首先要通过遍历查找到索引index对应的节点,因为链表时间复杂度为O(N),所以LinkedList的查找是效率极低的操作,查找方法为node(int index),在node方法中,做了二分处理,使得复杂度变为O(n)/2,源码如下:
Node<E> node(int 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; } }
添加方法的最终形式可归结为三种插入方式,即头插、尾插和中间插入。
在头部插入时,会依据上图中头部插入的步骤,源码如下:
// LinkedList.java 125行
private void linkFirst(E e) { final Node<E> f = first; // ① 和 ②,创建新节点,上一个元素prev = null, 下一个节点指向 first final Node<E> newNode = new Node<>(null, e, f); // ③ 头节点变为新节点 first = newNode; if (f == null) last = newNode; else // ④ 第二个节点(原头节点)的上一个节点指向新节点 f.prev = newNode; size++; modCount++; }
在尾部插入,会依据图中的尾部插入的步骤, 源码如下:
// LinkedList.java 140行
void linkLast(E e) { final Node<E> l = last; // ① 和 ② , 创建新节点。 上一个节点prev = last,下一个节点next = null final Node<E> newNode = new Node<>(l, e, null); // ③ 新节点变为尾节点 last = newNode; if (l == null) first = newNode; else // ④ 倒数第二个节点(原尾结点)的下一个节点指向新节点 l.next = newNode; size++; modCount++; }
中间插入源码如下:
// LinkedList.java 155行
void linkBefore(E e, Node<E> succ) { final Node<E> pred = succ.prev; // 下一个节点 // ① 和 ②,创建新节点,prev = pred , next = succ final Node<E> newNode = new Node<>(pred, e, succ); // ③ 新节点与下一个节点prev关联 succ.prev = newNode; if (pred == null) first = newNode; else // ④ 新节点与上个节点的next关联 pred.next = newNode; size++; modCount++; }
三、删除元素
LinkedList删除和添加方法相对,删除指定元素时需要遍历LinkedList查找元素,删除元素的方法(remove、poll等)同样最终归结三种,删除头节点(unlinkFirst)、删除尾节点(unlinkLast)、删除中间节点(unlink),删除时,会打断被删除节点两边的链接,然后置空value,交由GC回收对象,实现过程如下图:
删除头节点源码如下:
// LinkedList.java 171行
// 删除头节点 private E unlinkFirst(Node<E> f) { final E element = f.item; // 获取下一个节点 final Node<E> next = f.next; // ① 置空头节点value和next f.item = null; f.next = null; // help GC // ② next变为新的头节点 first = next; if (next == null) last = null; else // ③ 打断prev链接 next.prev = null; size--; modCount++; return element; }
删除尾结点源码如下:
// LinkedList.java 190行
private E unlinkLast(Node<E> l) { final E element = l.item; // 获取尾结点上一个节点 final Node<E> prev = l.prev; // ① 置空头节点value和prev l.item = null; l.prev = null; // help GC // ② prev变为新的尾节点 last = prev; if (prev == null) first = null; else // ③ 打断next链接 prev.next = null; size--; modCount++; return element; }
删除中间节点源码如下:
// LinkedList.java 209行
E unlink(Node<E> x) { final E element = x.item; //当前元素 final Node<E> next = x.next; //下一个节点 final Node<E> prev = x.prev; // 上一个节点 if (prev == null) { //删除节点为头节点 first = next; } else { // ① 上一个节点的next指向被删除节点next prev.next = next; // ② 断开 prev x.prev = null; } if (next == null) { //删除节点为尾结点 last = prev; } else { // ③ 下一个节点的prev指向被删除节点的prev next.prev = prev; // ④ 断开 next x.next = null; } // 置空元素值 x.item = null; size--; modCount++; return element; }
四、队列和栈功能
LinkedList实现List接口的同时,也实现了Deque接口,Deque是双端队列,队列是先进先出的数据结构,Deque在Queue的offset和poll基础上增加了offerFirst、pollLast等方法,实现了两边都能出队和入队的操作,所以LinkedList也可以做队列来使用。同时,由于可以插入头节点和取出头节点,也可用作栈,作为队列和栈时,出入队和出入栈的操作都是基于addFirst、addLast、unLinkFirst、unLinklast四个方法实现,源码就不在分析。