前文传送门:Java小白集合源码的学习系列:ArrayListjava
本篇为集合源码学习系列的LinkedList
学习部分,若有叙述不当之处,还望评论区批评指正!node
LinkedList和ArrayList同样,都实现了List接口,都表明着列表结构,都有着相似的add,remove,clear等操做。与ArrayList不一样的是,LinkedList底层基于双向链表,容许不连续地址的存储,经过节点之间的相互引用创建联系,经过节点存储数据。
数组
既然是基于节点的,那么咱们来看看节点在LinkedList中是怎样的存在:安全
//Node做为LinkedList的静态内部类 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做为其内部类,拥有三个属性,一个是用来指向前一节点的指针prev,一个是指向后一节点的指针next,还有存储的元素值item。
咱们来看看LinkedList的几个基本属性:学习
/*用transient关键字标记的成员变量不参与序列化过程*/ transient int size = 0;//记录节点个数 /** * first是指向第一个节点的指针。永远只有下面两种状况: * 一、链表为空,此时first和last同时为空。 * 二、链表不为空,此时第一个节点不为空,第一个节点的prev指针指向空 */ transient Node<E> first; /** * last是指向最后一个节点的指针,一样地,也只有两种状况: * 一、链表为空,first和last同时为空 * 二、链表不为空,此时最后一个节点不为空,其next指向空 */ transient Node<E> last; //须要注意的是,当first和last指向同一节点时,代表链表中只有一个节点。
了解基本属性以后,咱们看看它的构造方法,因为没必要在意它存储的位置,它的构造器也是至关简单的:this
//建立一个空链表 public LinkedList() { } //建立一个链表,包含指定传入的全部元素,这些元素按照迭代顺序排列 public LinkedList(Collection<? extends E> c) { this(); //添加操做 addAll(c); }
其中addAll(c)其实调用了addAll(size,c),因为这里size=0,因此至关于从头开始一一添加。至于addAll方法,咱们暂时不提,当咱们总结完普通的添加操做,也就天然明了这个所有添加的操做。线程
//把e做为链表的第一个元素 private void linkFirst(E e) { //创建临时节点指向first final Node<E> f = first; //建立存储e的新节点,prev指向null,next指向临时节点 final Node<E> newNode = new Node<>(null, e, f); //这时newNode变成了第一个节点,将first指向它 first = newNode; //对原来的first,也就是如今的临时节点f进行判断 if (f == null) //原来的first为null,说明原来没有节点,如今的newNode //是惟一的节点,因此让last也只想newNode last = newNode; else //原来链表不为空,让原来头节点的prev指向newNode f.prev = newNode; //节点数量加一 size++; //对列表进行改动,modCount计数加一 modCount++; }
相应的,把元素做为链表的最后一个元素添加和第一个元素添加方法相似,就不赘述了。咱们来看看咱们一开始遇到的addAll操做,感受有一点点麻烦的哦:指针
//在指定位置把另外一个集合中的全部元素按照迭代顺序添加进来,若是发生改变,返回true public boolean addAll(int index, Collection<? extends E> c) { //范围判断 checkPositionIndex(index); //将集合转换为数组,果传入集合为null,会出现空指针异常 Object[] a = c.toArray(); //传入集合元素个数为0,没有改变原集合,返回false int numNew = a.length; if (numNew == 0) return false; //建立两个临时节点,暂时表示新表的头和尾 Node<E> pred, succ; //至关于从原集合的尾部添加 if (index == size) { //暂时让succ置空 succ = null; //让pred指向原集合的最后一个节点 pred = last; } else { //若是从中间插入,则让succ指向指定索引位置上的节点 succ = node(index); //让succ的prev指向pred pred = succ.prev; } //加强for循环遍历赋值 for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; //建立存储值尾e的新节点,前向指针指向pred,后向指针指向null Node<E> newNode = new Node<>(pred, e, null); //代表原链表为空,此时让first指向新节点 if (pred == null); first = newNode; else //原链表不为空,就让临时节点pred节点向后移动 pred.next = newNode; //更新新表的头节点为当前新建立的节点 pred = newNode; } //这种状况出如今原链表后面插入 if (succ == null) { //此时pred就是最终链表的last last = pred; } else { //在index处插入的状况 //因为succ是node(index)的临时节点,pred由于遍历也到了插入链表的最后一个节点 //让最后位置的pred和succ创建联系 pred.next = succ; succ.prev = pred; } //新长度为原长+增加 size += numNew; modCount++; return true; }
再来看看,在链表中普通删除元素的操做是怎么样的:code
//取消一个非空节点x的连结,并返回它 E unlink(Node<E> x) { //一样的,在调用这个方法以前,须要确保x不为空 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; //明确x与上一节点的联系,更新并删除无用联系 //x为头节点 if (prev == null) { //让first指向x.next的临时节点next,宣布从下一节点开始才是头 first = next; } else { //x不是头节点的状况 //让x.prev的临时节点prev的next指向x.next的临时节点 prev.next = next; //删除x的前向引用,即让x.prev置空 x.prev = null; } //明确x与下一节点的联系,更新并删除无用联系 //x为尾节点 if (next == null) { //让last指向x.prev的临时节点prev,宣布上一节点是最后的尾 last = prev; } else { //x不是尾节点的状况 //让x.next的临时节点next的prev指向x.prev的临时节点 next.prev = prev; //删除x的后向引用,让x.next置空 x.next = null; } //让x存储元素置空,等待GC宠信 x.item = null; size--; modCount++; return element; }
总结来讲,删除操做无非就是,消除该节点与另外两个节点的联系,并让与它相邻的两个节点之间创建联系。若是考虑边界条件的话,好比为头节点和尾节点的状况,须要再另加分析。总之,它不须要向ArrayList同样,拷贝数组,而是改变节点间的地址引用。可是,删除以前须要找到这个节点,咱们仍是须要遍历滴,就像下面这样:
//移除第一次出现的元素o,找到并移除返回true,不然false public boolean remove(Object o) { //传入元素自己就为null if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { //调用上面提到的取消节点连结的方法 unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { //删除的元素不为null,比较值的大小 if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
总结一下从前向后遍历的过程:
- 建立一个临时节点指向first。
- 向后遍历,让临时节点指向它的下一位。
- 直到临时节点指向last的下一位(即x==null)为止。
固然特殊状况特殊考虑,上面的remove方法目的是找到对应的元素,只须要在循环中加入相应的逻辑判断便可。下面这个至关重要的辅助方法就是经过遍历获取指定位置上的节点:有了这个方法,咱们就能够同过它的先后位置,推导出其余不一样的方法:
//得到指定位置上的非空节点 Node<E> node(int index) { //在调用这个方法以前会确保0<=inedx<size //index和size>>1比较,若是index比size的一半小,从前向后遍历 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; //退出循环的条件,i==indx,此时x为当前节点 return x; } else { //从后向前遍历 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
与此同时还有indexOf和lastIndexOf方法也是经过上面总结的遍历过程,加上计数条件,计算出指定元素第一次或者最后一次出现的索引,这里以indexOf为例:
//返回元素第一次出现的位置,没找到就返回-1 public int indexOf(Object o) { int index = 0; if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; }
其实就是咱们上面讲的遍历操做嘛,大差不差。有了这个方法,咱们仍是能够很轻松地推导出另外的contains方法。
public boolean contains(Object o) { return indexOf(o) != -1; }
而后仍是那对基佬方法:get和set。
//获取元素值 public E get(int index) { checkElementIndex(index); return node(index).item; } //用新值替换旧值,返回旧值 public E set(int index, E element) { checkElementIndex(index); //获取节点 Node<E> x = node(index); //存取旧值 E oldVal = x.item; //替换旧值 x.item = element; //返回旧值 return oldVal; }
接下来是咱们的clear方法,移除全部的元素,将表置空。虽然写法有所不一样,可是基本思想是不变的:建立节点,并移动,删除不要的,或者找到须要的,就好了。
public void clear() { for (Node<E> x = first; x != null; ) { //建立临时节点指向当前节点的下一位 Node<E> next = x.next; //下面就能够安心地把当前节点有关的所有清除 x.item = null; x.next = null; x.prev = null; //x向后移动 x = next; } //回到最初的起点 first = last = null; size = 0; modCount++; }
咱们还知道,LinkedList还继承了Deque接口,让咱们可以操做队列同样操做它,下面是截取不彻底的一些方法:
咱们从中挑选几个分析一下,几个具备迷惑性方法的差别,好比下面这四个:
public E element() { return getFirst(); } public E getFirst() { final Node<E> f = first; //若是头节点为空,抛出异常 if (f == null) throw new NoSuchElementException(); return f.item; } public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; } public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item; }
与之相似的还有:
若是有兴趣的话,能够研究一下,总之仍是相对简单的。
而LinkedList底层基于双向链表实现,不须要连续的内存存储,经过节点之间相互引用地址造成联系。
对于无索引位置的插入来讲,例如向后插入,时间复杂度近似为O(1),体现出增删操做较快。可是若是要在指定的位置上插入,仍是须要移动到当前指定索引位置,才能够进行操做,时间复杂度近似为O(n)。
Linkedlist不支持快速随机访问,查询较慢。
线程不安全,一样的,关于线程方面,之后学习时再进行总结。