前言:LinkedList的底层数据结构是双向链表,下面具体分析其实现原理。
注:本文jdk源码版本为jdk1.8.0_172
1..LinkedList介绍
LinkedList继承于AbstractSequentialList的双向链表,实现List接口,因此也可以对其进行队列操作,它也实现了Deque接口,所以LinkedList也可当做双端队列使用,还有LinkedList是非同步的。
1 java.lang.Object 2 ↳ java.util.AbstractCollection<E> 3 ↳ java.util.AbstractList<E> 4 ↳ java.util.AbstractSequentialList<E> 5 ↳ java.util.LinkedList<E> 6 7 public class LinkedList<E> 8 extends AbstractSequentialList<E> 9 implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}
由于LinkedList的底层是双向链表,因此其顺序访问的效率非常高,而随机访问的效率就比较低了,因为通过索引去访问的时候,首先会比较索引值和链表长度的1/2,若前者大,则从链表尾开始寻找,否则从链表头开始寻找,这样就把双向链表与索引值联系起来了。
2.具体源码分析
LinkedList底层数据结构:
1 private static class Node<E> { 2 E item; 3 Node<E> next; 4 Node<E> prev; 5 6 Node(Node<E> prev, E element, Node<E> next) { 7 this.item = element; 8 this.next = next; 9 this.prev = prev; 10 } 11 }
分析:Node为LinkedList的底层数据结构,关联了前驱节点,后续节点和值。
构造函数,LinkedList提供了两个构造函数:
1 public LinkedList() { 2 } 3 public LinkedList(Collection<? extends E> c) { 4 this(); 5 addAll(c); 6 }
add函数,添加元素时,是直接添加在链表的结尾:
1 public boolean add(E e) { 2 linkLast(e); 3 return true; 4 } 5 void linkLast(E e) { 6 // 取出当前最后一个节点 7 final Node<E> l = last; 8 // 创建一个新节点,注意其前驱节点为l,后续节点为null 9 final Node<E> newNode = new Node<>(l, e, null); 10 // 记录新的最后一个节点 11 last = newNode; 12 // 如果最后一个节点为空,则表示链表为空,则将first节点也赋值为newNode 13 if (l == null) 14 first = newNode; 15 else 16 // 关联l的next节点,构成双向节点 17 l.next = newNode; 18 // 元素总数加1 19 size++; 20 // 修改次数自增 21 modCount++; 22 }
分析:
从源码上可以非常清楚的了解LinkedList加入元素是直接放在链表尾的,主要点构成双向链表,整体逻辑并不复杂,通过上述注释理解应该不成问题。
add(int,element),在具体index上插入元素:
1 public void add(int index, E element) { 2 // 校验index是否越界 3 checkPositionIndex(index); 4 // index和size相同则,添加在链表尾 5 if (index == size) 6 linkLast(element); 7 else 8 // 在index位置前插入元素 9 linkBefore(element, node(index)); 10 } 11 // Inserts element e before non-null Node succ. 12 void linkBefore(E e, Node<E> succ) { 13 // assert succ != null; 14 final Node<E> pred = succ.prev; 15 // 创建新的节点 前驱节点为succ的前驱节点,后续节点为succ,则e元素就是插入在succ之前的 16 final Node<E> newNode = new Node<>(pred, e, succ); 17 // 构建双向链表,succ的前驱节点为新节点 18 succ.prev = newNode; 19 // 如果前驱节点为空,则first为newNode 20 if (pred == null) 21 first = newNode; 22 else 23 // 构建双向列表 24 pred.next = newNode; 25 // 元素总数自增 26 size++; 27 // 修改次数自增 28 modCount++; 29 }
分析:该函数并不是直接插入链表尾,需要进行一个判断,逻辑并不复杂,通过注释应该不难理解,但这里要注意一个函数node(index),取出对应index上的Node元素,下面来具体分析一下。
1 Node<E> node(int index) { 2 // assert isElementIndex(index); 3 // 因为这里的x不是next就是prev,当循环中止时,就是对应index上的值 4 // index如果小于链表长度的1/2 5 if (index < (size >> 1)) { 6 Node<E> x = first; 7 // 从链表头开始移动 8 for (int i = 0; i < index; i++) 9 x = x.next; 10 return x; 11 } else { 12 // 从链表尾开始移动 13 Node<E> x = last; 14 for (int i = size - 1; i > index; i--) 15 x = x.prev; 16 return x; 17 } 18 }
接下来看构造函数中的addAll方法:
1 public boolean addAll(int index, Collection<? extends E> c) { 2 // 检查index是否越界 3 checkPositionIndex(index); 4 5 Object[] a = c.toArray(); 6 int numNew = a.length; 7 // 如果插入集合无数据,则直接返回 8 if (numNew == 0) 9 return false; 10 11 // succ的前驱节点 12 Node<E> pred, succ; 13 // 如果index与size相同 14 if (index == size) { 15 // succ的前驱节点直接赋值为最后节点 16 // succ赋值为null,因为index在链表最后 17 succ = null; 18 pred = last; 19 } else { 20 // 取出index上的节点 21 succ = node(index); 22 pred = succ.prev; 23 } 24 // 遍历插入集合 25 for (Object o : a) { 26 @SuppressWarnings("unchecked") E e = (E) o; 27 // 创建新节点 前驱节点为succ的前驱节点,后续节点为null 28 Node<E> newNode = new Node<>(pred, e, null); 29 // succ的前驱节点为空,则表示succ为头,则重新赋值第一个结点 30 if (pred == null) 31 first = newNode; 32 else 33 // 构建双向链表 34 pred.next = newNode; 35 // 将前驱节点移动到新节点上,继续循环 36 pred = newNode; 37 } 38 39 // index位置上为空 赋值last节点为pred,因为通过上述的循环pred已经走到最后了 40 if (succ == null) { 41 last = pred; 42 } else { 43 // 构建双向链表 44 // 从这里可以看出插入集合是在succ[index位置上的节点]之前 45 pred.next = succ; 46 succ.prev = pred; 47 } 48 // 元素总数更新 49 size += numNew; 50 // 修改次数自增 51 modCount++; 52 return true; 53 }
分析:逻辑并不复杂,注意一点即可,插入集合的元素是在index元素之前。
其他重要的源码分析:
1 // 通过index获取元素 2 public E get(int index) { 3 // 检查index是否越界 4 checkElementIndex(index); 5 // 通过node函数返回节点值 node函数前面已经分析过 6 return node(index).item; 7 } 8 9 // 增加元素在链表头位置 10 private void linkFirst(E e) { 11 final Node<E> f = first; 12 // 创建新节点 前驱节点为null,后续节点为first节点 13 final Node<E> newNode = new Node<>(null, e, f); 14 // 更新first节点 15 first = newNode; 16 // 如果f为空,表示原来为空,更新last节点为新节点 17 if (f == null) 18 last = newNode; 19 else 20 // 构建双向链表 21 f.prev = newNode; 22 // 元素总数自增 23 size++; 24 // 修改次数自增 25 modCount++; 26 } 27 28 // 释放头节点 29 private E unlinkFirst(Node<E> f) { 30 // assert f == first && f != null; 31 final E element = f.item; 32 final Node<E> next = f.next; 33 f.item = null; 34 f.next = null; // help GC 35 // 更新头节点 36 first = next; 37 if (next == null) 38 last = null; 39 else 40 // 将头节点的前驱节点赋值为null 41 next.prev = null; 42 // 元素总数自减 43 size--; 44 // 修改次数自增 45 modCount++; 46 // 返回删除的节点数据 47 return element; 48 } 49 // 释放尾节点 50 private E unlinkLast(Node<E> l) { 51 // assert l == last && l != null; 52 final E element = l.item; 53 // 和释放头节点相反,这里取出前驱节点,其他逻辑一样 54 final Node<E> prev = l.prev; 55 l.item = null; 56 l.prev = null; // help GC 57 last = prev; 58 if (prev == null) 59 first = null; 60 else 61 prev.next = null; 62 size--; 63 modCount++; 64 return element; 65 }
3.总结
整体分析下来,其实LinkedList还是比较简单的,上面对一些重要的相关源码进行了分析,主要重点如下:
#1.LinkedList底层数据结构为双向链表,非同步。
#2.LinkedList允许null值。
#3.由于双向链表,顺序访问效率高,而随机访问效率较低。
#4.注意源码中的相关操作,主要是构建双向链表。
by Shawn Chen,2019.09.02日,上午。