LinkedList类源码剖析 一、LinkedList类的简介 LinkedList
类是List
和Deque
接口的双向链表
实现,实现所有可选列表操作,并允许所有元素(包括null)。所有操作的执行都与双向链表的预期相同。对列表进行索引的操作将从开头或结尾遍历列表,以更接近指定索引的为准。LinkedList
是一个基于双向链表实现的集合类,经常被拿来和ArrayList
做比较,除了支持快速随机访问和自动扩容机制外(LinkedList底层采用双向链表,不需要预分配存储空间),LinkedList与ArrayList的功能特性基本一致,仅仅多了一些双端队列的操作。
不过,我们在项目中一般是不会使用到LinkedList
的,需要用到LinkedList
的场景几乎都可以使用ArrayList
来代替,并且性能通常会更好!另外,不要下意识地认为LinkedList
作为链表就最适合元素增删的场景,LinkedList
仅仅在头尾插入或者删除元素的时候时间复杂度近似O(1),其他情况增删元素的平均时间复杂度都是O(n)。
LinkedList插入和删除元素的时间复杂度?
头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为O(1)。
尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为O(1)。
指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均n/2个元素,时间复杂度为O(n)。
LinkedList为什么不能实现RandomAccess接口?
RandomAccess
是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于LinkedList
底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现RandomAccess
接口。
1 2 3 4 5 6 public class LinkedList <E> extends AbstractSequentialList <E> implements List <E>, Deque<E>, Cloneable, java.io.Serializable { ... }
LinkedList
继承了AbstractSequentialList
,而AbstractSequentialList
又继承于AbstractList
。阅读过ArrayList
的源码我们就知道,ArrayList
同样继承了AbstractList
,所以LinkedList
会有大部分方法和ArrayList
相似。
LinkedList
实现了以下接口:
List
:表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
Deque
:继承自Queue
接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。
Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
Serializable
:表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
LinkedList
中的元素是通过Node
定义的:
1 2 3 4 5 6 7 8 9 10 11 12 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; } }
二、LinkedList类的字段 1 2 3 4 5 6 transient int size = 0 ;transient Node<E> first;transient Node<E> last;
LinkedList类的字段非常简介明朗,其中size记录了存储的元素个数,first和last记录了底层双向链表的首尾节点。
三、LinkedList类的构造函数 LinkedList
中有一个无参构造函数和一个有参构造函数。其中有参构造函数通过addAll
函数将集合中的元素都一一添加到链表末尾。
1 2 3 4 5 6 7 8 9 public LinkedList () {} public LinkedList (Collection<? extends E> c) { this (); addAll(c); }
四、LinkedList类的插入操作 LinkedList
除了实现了List
接口相关方法,还实现了Deque
接口的很多方法,所以我们有很多种方式插入元素。我们这里以List
接口中相关的插入方法为例进行源码讲解,对应的是add()
方法。
add()
方法有两个版本:
add(E e)
:用于在LinkedList
的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为O(1)。
add(int index, E element)
:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均n/2个元素,时间复杂度为O(n)。
此外,作为双向链表,LinkedList还支持两端的元素插入操作,即addFirst
和addLast
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 public boolean add (E e) { linkLast(e); return true ; } public void add (int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } public void addFirst (E e) { linkFirst(e); } public void addLast (E e) { linkLast(e); } private void linkFirst (E e) { final Node<E> f = first; final Node<E> newNode = new Node <>(null , e, f); first = newNode; if (f == null ) last = newNode; else f.prev = newNode; size++; modCount++; } 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; else l.next = newNode; size++; modCount++; } void linkBefore (E e, Node<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; else pred.next = newNode; size++; modCount++; }
如果大家细心,前面讨论LinkedList类的构造函数时提到了一个addAll
方法,将指定集合中的所有元素按照指定集合的迭代器返回的顺序附加到此列表的末尾。如果在操作进行过程中修改了指定的集合,则该操作的行为是未定义
的。(请注意,如果指定的集合是此列表,并且它非空,则会发生这种情况。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public boolean addAll (Collection<? extends E> c) { return addAll(size, c); } public boolean addAll (int index, Collection<? extends E> c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0 ) return false ; Node<E> pred, succ; if (index == size) { succ = null ; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node <>(pred, e, null ); if (pred == null ) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null ) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true ; }
五、LinkedList类的获取操作 LinkedList
获取元素相关的方法一共有3个:
getFirst()
:获取链表的第一个元素。
getLast()
:获取链表的最后一个元素。
get(int index)
:获取链表指定位置的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public E getFirst () { final Node<E> f = first; if (f == null ) throw new NoSuchElementException (); return f.item; } public E getLast () { final Node<E> l = last; if (l == null ) throw new NoSuchElementException (); return l.item; } public E get (int index) { checkElementIndex(index); return node(index).item; }
这里的核心在于node(int index)
这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; } }
get(int index)
或remove(int index)
等方法内部都调用了该方法来获取对应的节点。
从这个方法的源码可以看出,该方法通过比较索引值与链表size的一半大小来确定从链表头还是尾开始遍历。如果索引值小于size的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。
六、LinkedList类的删除操作 LinkedList
删除元素相关的方法一共有5个:
removeFirst()
:删除并返回链表的第一个元素。
removeLast()
:删除并返回链表的最后一个元素。
remove(E e)
:删除链表中首次出现的指定元素,如果不存在该元素则返回false。
remove(int index)
:删除指定索引处的元素,并返回该元素的值。
void clear()
:移除此链表中的所有元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public E removeFirst () { final Node<E> f = first; if (f == null ) throw new NoSuchElementException (); return unlinkFirst(f); } public E removeLast () { final Node<E> l = last; if (l == null ) throw new NoSuchElementException (); return unlinkLast(l); } public boolean remove (Object o) { 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) { if (o.equals(x.item)) { unlink(x); return true ; } } } return false ; } public E remove (int index) { checkElementIndex(index); return unlink(node(index)); }
这里的核心在于unlink(Node<E> x)
这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 { prev.next = next; x.prev = null ; } if (next == null ) { last = prev; } else { next.prev = prev; x.next = null ; } x.item = null ; size--; modCount++; return element; }
unlink()
方法的逻辑如下:
首先获取待删除节点x的前驱和后继节点;
判断待删除节点是否为头节点或尾节点:
如果x是头节点,则将first指向x的后继节点next
如果x是尾节点,则将last指向x的前驱节点 prev
如果x不是头节点也不是尾节点,执行下一步操作
将待删除节点x的前驱的后继指向待删除节点的后继next,断开x和x.prev之间的链接;
将待删除节点x的后继的前驱指向待删除节点的前驱prev,断开x和x.next之间的链接;
将待删除节点x的元素置空,修改链表长度。
除了unlink
方法外,还有unlinkFirst
和unlinkLast
两个方法,相对比较简单,这三个方法组成了双向链表中最底层的删除操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 private E unlinkFirst (Node<E> f) { final E element = f.item; final Node<E> next = f.next; f.item = null ; f.next = null ; first = next; if (next == null ) last = null ; else next.prev = null ; size--; modCount++; return element; } private E unlinkLast (Node<E> l) { final E element = l.item; final Node<E> prev = l.prev; l.item = null ; l.prev = null ; last = prev; if (prev == null ) first = null ; else prev.next = null ; size--; modCount++; return element; }
七、LinkedList类的遍历操作 推荐使用for-each
循环来遍历LinkedList
中的元素,for-each
循环最终会转换成迭代器形式。LinkedList
的遍历的核心就是它的迭代器的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private class ListItr implements ListIterator <E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { next = (index == size) ? null : node(index); nextIndex = index; } }
下面我们对迭代器ListItr
中的核心方法进行详细介绍。
我们先来看下从头到尾方向的迭代:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public boolean hasNext () { return nextIndex < size; } public E next () { checkForComodification(); if (!hasNext()) throw new NoSuchElementException (); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; }
再来看一下从尾到头方向的迭代:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public boolean hasPrevious () { return nextIndex > 0 ; } public E previous () { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException (); lastReturned = next = (next == null ) ? last : next.prev; nextIndex--; return lastReturned.item; }
迭代器对应的移除元素的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void remove () { checkForComodification(); if (lastReturned == null ) throw new IllegalStateException (); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null ; expectedModCount++; }
八、LinkedList类的队列操作对比 首先查看LinkedList的继承关系可知,LinkedList实现了双端队列接口Deque,而双端队列接口Deque又继承了队列接口Queue,因此LinkedList既可以作为一个普通队列使用,也可以作为双端队列使用。
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Queue
扩展了 Collection
的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Queue
接口
抛出异常
返回特殊值
插入队尾
add(E e)
offer(E e)
删除队首
remove()
poll()
查询队首元素
element()
peek()
我们根据源码先查看抛出异常的三个队列方法:add
、remove
、element
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public boolean add (E e) { linkLast(e); return true ; } public E remove () { return removeFirst(); } public E element () { return getFirst(); }
队列的add方法不一定会抛出异常,这取决于具体队列的实现类,对于LinkedList来说不存在容量限制和元素值限制(允许null),因此不会抛出任何异常。
然后,我们再查看返回特殊值的三个队列方法:offer
、poll
、peek
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean offer (E e) { return add(e); } public E poll () { final Node<E> f = first; return (f == null ) ? null : unlinkFirst(f); } public E peek () { final Node<E> f = first; return (f == null ) ? null : f.item; }
九、LinkedList类的双端队列操作 Deque
是双端队列,在队列的两端均可以插入或删除元素。
Deque
扩展了 Queue
的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
Deque
接口
抛出异常
返回特殊值
插入队首
addFirst(E e)
offerFirst(E e)
插入队尾
addLast(E e)
offerLast(E e)
删除队首
removeFirst()
pollFirst()
删除队尾
removeLast()
pollLast()
查询队首元素
getFirst()
peekFirst()
查询队尾元素
getLast()
peekLast()
首先,我们还是先看看抛出异常的6个双端队列方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public void addFirst (E e) { linkFirst(e); } public void addLast (E e) { linkLast(e); } public E removeFirst () { final Node<E> f = first; if (f == null ) throw new NoSuchElementException (); return unlinkFirst(f); } public E removeLast () { final Node<E> l = last; if (l == null ) throw new NoSuchElementException (); return unlinkLast(l); } public E getFirst () { final Node<E> f = first; if (f == null ) throw new NoSuchElementException (); return f.item; } public E getLast () { final Node<E> l = last; if (l == null ) throw new NoSuchElementException (); return l.item; }
然后,我们看看返回特殊值的6个双端队列方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public boolean offerFirst (E e) { addFirst(e); return true ; } public boolean offerLast (E e) { addLast(e); return true ; } public E pollFirst () { final Node<E> f = first; return (f == null ) ? null : unlinkFirst(f); } public E pollLast () { final Node<E> l = last; return (l == null ) ? null : unlinkLast(l); } public E peekFirst () { final Node<E> f = first; return (f == null ) ? null : f.item; } public E peekLast () { final Node<E> l = last; return (l == null ) ? null : l.item; }
事实上,Deque
还提供有 push()
和 pop()
等其他方法,可用于模拟栈。
十、LinkedList类常用方法测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 LinkedList<String> list = new LinkedList <>(); list.add("apple" ); list.add("banana" ); list.add("pear" ); System.out.println("链表内容:" + list); list.add(1 , "orange" ); System.out.println("链表内容:" + list); String fruit = list.get(2 );System.out.println("索引为 2 的元素:" + fruit); list.set(3 , "grape" ); System.out.println("链表内容:" + list); list.remove(0 ); System.out.println("链表内容:" + list); list.remove("banana" ); System.out.println("链表内容:" + list); int size = list.size();System.out.println("链表长度:" + size); list.clear(); System.out.println("清空后的链表:" + list);
参考文章:LinkedList源码分析