声明:尊重他人劳动成果,转载请附带原文链接!
1.LinkedList继承体系
LinkedList:LinkedList是一个以双向链表实现的List,它除了作为List使用,还可以作为队列或者栈来使用。
从继承体系可以看出,LinkedList实现了Cloneable和Serializable接口,说明其可以被克隆,也可以被序列化!同样的,LinkedList被克隆的时候,和ArrayList一样二者均是浅拷贝。
对于如何实现集合的浅拷贝和序列化,我在上一篇文章JDK集合源码之ArrayList解析已经给大家介绍了,可以参考一下。
下面进入正题——分析源码:
2.LinkedList基本属性
// 链表中元素的个数 transient int size = 0; // 链表的头节点 transient Node<E> first; // 链表的尾节点 transient Node<E> last;
三个基本属性通过关键字transient修饰,使其不被序列化。
3.Node内部类
private static class Node<E> { E item;// node存储的元素 Node<E> next;// 前驱 Node<E> prev;// 后驱 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
从代码中就可以看出,这是双向链表结构。
4.构造方法
空参构造
public LinkedList() { }
有参构造(参数是一个Collection)
public LinkedList(Collection<? extends E> c) { this(); addAll(c); } // 将指定集合中的所有元素追加到此列表的末尾 public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } // 从指定位置开始,将指定集合中的所有元素插入此集合 public boolean addAll(int index, Collection<? extends E> c) {// 从指定位置开始,将指定集合中的所有元素插入此集合 checkPositionIndex(index);// 检查index是否越界,index >= 0 && index <= size Object[] a = c.toArray();// 将集合转成object类型数组 int numNew = a.length;// 获取该数组长度 if (numNew == 0) return false; Node<E> pred, succ; if (index == size) {// 当要插入位置的索引位于链表最后一个元素处 succ = null;// 当前节点置空 pred = last;// 要拼接的集合前驱节点为原始链表的尾节点 } else { succ = node(index);// 得到index索引位置的节点 pred = succ.prev;// 前驱节点指向index所处节点的前一个节点 } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null)// 如果前驱为null说明当前节点位于该链表的头节点 first = newNode;// 使头节点等于新创建的节点 else pred.next = newNode;// 前驱节点的尾指针指向头节点 pred = newNode;// pred前驱后移 } if (succ == null) {// 这种情况说明index索引位于原始链表的最后 last = pred;// 尾节点直接指向拼接完成链表的最后 } else { pred.next = succ;// pred的后驱节点是succ succ.prev = pred;// succ的前驱节点是pred } size += numNew;// 集合中元素的个数+numNew modCount++;// 集合修改次数+1 return true; } // 返回指定元素索引处的(非空)节点。 Node<E> node(int index) { if (index < (size >> 1)) {// 如果该索引位于链表元素的前一半 Node<E> x = first;// 从头节点开始定位index的位置 for (int i = 0; i < index; i++) x = x.next;// 指针移动到index位置 return x;// 将该节点内容返回 } else {// 如果该索引位于链表元素的后一半 Node<E> x = last;// 从尾节点开始定位index的位置 for (int i = size - 1; i > index; i--) x = x.prev;// 指针移动到index位置 return x;// 将该节点内容返回 } }
由两个无参构造方法得出,这是一个无界的双端队列。
5.添加元素
对于双端队列的性质,添加元素时,一种是在队列尾部添加元素,一种是在队列首部添加元素,这两种形式在LinkedList中主要是通过下面两个方法来实现的:
public void addFirst(E e) { linkFirst(e);// 头部添加 } public void addLast(E e) { linkLast(e);// 尾部添加 } // 作为无界队列,添加元素总是会成功的 public boolean offerFirst(E e) { addFirst(e); return true; } public boolean offerLast(E e) { addLast(e); return true; }
头部添加
/** * 头部添加元素 */ private void linkFirst(E e) { final Node<E> f = first;// 首节点 final Node<E> newNode = new Node<>(null, e, f);// 创建新节点,新节点的next是首节点 first = newNode;// 让新节点作为新的首节点 if (f == null)// 判断是不是第一个添加的元素 last = newNode;// 如果是就把last也设置为新节点 else// 如果不是,就把原首节点的prev指针置为新节点 f.prev = newNode;// 原来首节点的前驱是当前新增节点 size++;// 元素个数加1 modCount++;// 修改次数加1,说明这是一个支持fail-fast的集合 }
尾部添加
/** * 尾部添加 */ void linkLast(E e) { final Node<E> l = last;// 队列尾节点 final Node<E> newNode = new Node<>(l, e, null);// 创建新节点,新节点的prev是尾节点 last = newNode;// 让新节点成为新的尾节点 if (l == null)// 判断是不是第一个添加的元素 first = newNode;// 如果是就把first也置为新节点 else// 否则把原尾节点的next指针置为新节点 l.next = newNode; size++;// 元素个数加1 modCount++;// 修改次数加1,说明这是一个支持fail-fast的集合
中间指定位置添加
LinedList作为集合,需要在中间其他位置添加元素,该功能是通过下面的方法实现的:
// 在指定index位置处添加元素 public void add(int index, E element) { checkPositionIndex(index);// 判断是否越界, if (index == size)// 如果index是在队列尾节点之后的一个位置 linkLast(element);// 把新节点直接添加到尾节点之后 else// 否则调用linkBefore()方法在中间添加节点 linkBefore(element, node(index)); } // 寻找index位置的节点,返回指定元素索引处的(非空)节点。 Node<E> node(int index) { // 因为是双链表 if (index < (size >> 1)) {// 如果该索引位于链表元素的前一半 Node<E> x = first;// 从头节点开始定位index的位置 for (int i = 0; i < index; i++) x = x.next;// 指针移动到index位置 return x;// 将该节点内容返回 } else {// 如果该索引位于链表元素的后一半 Node<E> x = last;// 从尾节点开始定位index的位置 for (int i = size - 1; i > index; i--) x = x.prev;// 指针移动到index位置 return x;// 将该节点内容返回 } } void linkBefore(E e, Node<E> succ) {// 在非空节点succ之前插入元素e // succ是待添加节点的后继节点 final Node<E> pred = succ.prev;// 找到待添加节点的前置节点 final Node<E> newNode = new Node<>(pred, e, succ);// 在其前置节点和后继节点之间创建一个新节点 succ.prev = newNode;// 修改后继节点的前置指针指向新节点 if (pred == null)// 判断前置节点是否为空 first = newNode;// 如果为空,说明是第一个添加的元素,修改first指针 else// 否则修改前置节点的next为新节点 pred.next = newNode; size++;// 修改集合元素个数 modCount++;// 集合修改次数加1 }
LinkedList在中间添加元素的方法实现原理就是,典型的双链表在中间添加元素的流程!
如图:
在队列首尾添加元素很高效,时间复杂度为O(1)
在中间添加元素比较低效,首先要先找到插入位置的节点,再修改前后节点的指针,时间复杂度为O(n)
6.删除元素
作为双端队列,删除元素也有两种方式,一种是队列首删除元素,一种是队列尾删除元素。
作为List,又要支持中间删除元素,所以删除元素一个有三个方法,分别如下。
移除头部元素:
头部删除
public E
public E removeFirst() {// remove的时候如果没有元素抛出异常 final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } private E unlinkFirst(Node<E> f) {// 删除首节点 // assert f == first && f != null; final E element = f.item;// 首节点的元素值 final Node<E> next = f.next;// 首节点的next指针 f.item = null; f.next = null; // 协助GC回收 first = next;// 把首节点的next作为新的首节点 if (next == null)// 如果只有一个元素,删除了,把last也置为空 last = null; else// 否则把next的前置指针置为空 next.prev = null; size--;// 元素个数减1 modCount++;// 修改次数加1 return element;// 返回删除的元素 }
尾部删除
public E removeLast() {// remove的时候如果没有元素抛出异常 final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } private E unlinkLast(Node<E> l) {// 删除尾节点 // assert l == last && l != null; final E element = l.item;// 尾节点的元素值 final Node<E> prev = l.prev;// 尾节点的前置指针 l.item = null; l.prev = null; // 清空尾节点的内容,协助GC last = prev;// 让前置节点成为新的尾节点 if (prev == null)// 如果只有一个元素,删除了把first置为空 first = null; else// 否则把前置节点的置为空next prev.next = null; size--;// 元素个数减1 modCount++;// 修改次数加1 return element;// 返回删除的元素 }
中间指定位置删除
public E remove(int index) {// 删除中间index处的节点 checkElementIndex(index);// 检查是否越界 return unlink(node(index));// 删除指定index位置的节点 } /** * 删除指定节点x. */ E unlink(Node<E> x) { // assert x != null; final E element = x.item;// x的元素值 final Node<E> next = x.next;// x的后置节点 final Node<E> prev = x.prev;// x的前置节点 if (prev == null) {// 如果前置节点为空 first = next;// 说明是首节点,让first指向x的后置节点 } else {// 否则修改前置节点的next为x的后置节点 prev.next = next; x.prev = null;// x的前置为空 } if (next == null) {// 如果后置节点为空 last = prev;// 说明是尾节点,让last指向x的前置节点 } else {// 否则修改后置节点的prev为x的前置节点 next.prev = prev; x.next = null;// x的后置为空 } x.item = null;// 清空x的元素值,协助GC size--;// 元素个数减1 modCount++;// 修改次数加1 return element;// 返回删除的元素 }
7.修改元素
根据索引修改元素很简单:
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
8.可以作为队列
另外,因为LinkedList本身除了继承于List,还继承于Deque,说明其也具有队列的peek(),poll()与其类似的就是pollFirst(),pollLast(),peekLast(),peekFirst()等,其实现原理相同,这里简单列举两个:
// poll的时候如果没有元素返回null public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } // poll的时候如果没有元素返回null public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); }
删除元素的三种方法都是典型的双链表删除元素的方法,大致流程如下图所示:
- 在队列首尾删除元素很高效,时间复杂度为O(1)。
- 在中间删除元素比较低效,首先要找到删除位置的节点,再修改前后指针,时间复杂度为O(n)。
9.可以作为栈
LinkedList是双端队列,双端队列可以作为栈使用。
其也具有如下方法:
// 弹出末尾元素 public E pop() { // 队列的头元素就是栈的末尾元素 return removeFirst(); } // 入栈 public void push(E e) { // 队列的头元素就是栈的末尾元素 addFirst(e); }
10.总结
1)LinkedList是一个以双链表实现的List;
2)LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
3)LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
4)LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
5)LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
6)LinkedList在功能上等于ArrayList + ArrayDeque;
ArrayList代表了List的典型实现,LInkedList代表了Deque的典型实现,同时LinkedList也实现了List。
11.扩展
LinkedList的并发修改异常
举例:
@Test public void test02() { LinkedList linkedList = new LinkedList(); linkedList.add("aaa"); linkedList.add("bbb"); linkedList.add("ccc"); Iterator iterator = linkedList.iterator(); while (iterator.hasNext()){// 使用迭代器遍历的时候,进行添加或者删除操作修改列表 linkedList.add("eee");// 会出现并发修改异常 System.out.println(iterator.next()); } }
在使用迭代器遍历的时候,又进行改动链表结构,就会出现并发修改异常错误:
java.util.ConcurrentModificationException at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:966) at java.util.LinkedList$ListItr.next(LinkedList.java:888) at com.haust.list.LinkedListTest.test02(LinkedListTest.java:35)
我们来研究下异常产生的原因:
首先,我们从迭代器linkedList.iterator();
入手,点进去看源码,逐层点击,进入AbstractList 类中,可以看到如下代码:
public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index);// 检查index是否越界 return new ListItr(index); }
我们点进去返回的ListItr(index)
查看:
// 这里的modCount=3,因为我们初始化linkedList集合的时候为其添加了三个元素 // 分别是:aaa bbb ccc // 令期望修改次数等于链表修改的次数 private int expectedModCount = modCount; // 所以现在expectedModCount 在执行完linkedList.iterator();之后其预期修改次数是3
当执行完linkedList.iterator();
之后进入while循环,循环条件是iterator.hasNext()
,之后进入循环体内部执行add添加操作,调用:
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last;// 队列尾节点 final Node<E> newNode = new Node<>(l, e, null);// 创建新 last = newNode;// 让新节点成为新的尾节点 if (l == null)// 判断是不是第一个添加的元素 first = newNode;// 如果是就把first也置为新节点 else// 否则把原尾节点的next指针置为新节点 l.next = newNode; size++;// 元素个数加1 /** 注意:这里添加完后,链表的修改次数加1,这是导致并发修改异常的重要条件 **/ modCount++;// 这里再加1后,实际链表修改次数由3--->变为4 }
add添加操作完成后,执行输出语句中的iterator.next()
,进入其next()源码,发现:
public E next() { // 检查链表预期修改次数和实际链表修改次数是否匹配 checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } final void checkForComodification() { // 这里实际修改次数4和期望修改次数不相等 if (modCount != expectedModCount) // 抛出并发修改异常 throw new ConcurrentModificationException(); }
解决方案:
// 解决方案:使用 Iterator 换用 ListIterator迭代器 ListIterator listIterator = linkedList.listIterator() while (listIterator.hasNext()){ listIterator.add("eee");// 用listIterator为集合添加元素 System.out.println(iterator.next()); }
输出结果(正常):
aaa bbb ccc
ListIterator
的listIterator()
和原来的iterator()
基本相同,而其之所以可以解决并发修改异常原因在于,listIterator.add("eee");
该代码段,其添加元素的流程如下:
public void add(E e) { // 添加之前先检查是否有并发修改异常,这时候预期修改次数和实际修改次数还是相等的,都是3 checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next);// 执行完成后modCount++变为4 nextIndex++; // 重点就在于此:当执行完添加后,将预期修改次数也+1,这样就能保证其与实际修改次数同步+1,二者目前都为4 expectedModCount++; }
如果文章对您有帮助,还请一键三连!