今天带给大家的是 List 集合大家庭下的--- LinkedList。
说到 LinkedList,那么我们大家的第一想法就是 链表,是插入删除快,随机访问慢,今天我们就来一探究竟,究竟内部的它是什么构造导致的问题,我们是否可以在使用的时候有一个更好的解决方案。
本系列文章基于 JDK 1.8
LinkedList :集合的子类
public class LinkedListextends AbstractSequentialList implements List , Deque , Cloneable, java.io.Serializable
首先,LinkedList 与 ArrayList 一样,实现了 List,而 List 又拓展了 Collection,而 Collection 又拓展了 Iterable,所以他们的一些公共方法是可以通用的,比如 迭代器方法。
而 LinkedList 还拓展了 Deque 队列接口,也就是我们平时说的 FIFO,先进先出。
该图引用自网络
从上图可以看出,我们今天要将的 LinkedList 继承了 AbstractSequentialList,而AbstractSequentialList 又继承了 AbstractList,AbstractList 继承 AbstractCollection 并且实现 List。所以,这是一个大的结构链。
这里我们来专门说下 AbstractCollection 和 AbstractSequentialList
-
AbstractCollection
public abstract class AbstractCollectionimplements Collection { protected AbstractCollection() {} public abstract Iterator iterator(); public abstract int size(); public boolean isEmpty() {} public boolean contains(Object o) {} public Object[] toArray() {} public T[] toArray(T[] a) {} private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static T[] finishToArray(T[] r, Iterator it) {} private static int hugeCapacity(int minCapacity) {} public boolean add(E e) {} public boolean remove(Object o) {} public boolean containsAll(Collection c) {} public boolean addAll(Collection c) {} public boolean removeAll(Collection c) {} public boolean retainAll(Collection c) {} public void clear() {} public String toString() {} }
AbstractCollection 是 Collection 接口的一个基本实现。
public boolean add(E e) { throw new UnsupportedOperationException(); } public abstract Iteratoriterator(); public abstract int size();
在 AbstractCollection 中,其实主要是依赖上面的 3 个方法,其中 add 方法抛出 不支持操作异常,如果子类是不可以修改的,则不用重写 add 方法即可。
size 方法和 iterator 方法是抽象方法,子类必须重写,当然,如果子类是不可修改的,则不用实现 remove 方法至于其他的像判断是否为空的 isEmpty 则是判断 size == 0。contains 方法则是调用 iterator 方法。
public boolean contains(Object o) { Iteratorit = iterator(); if (o==null) { while (it.hasNext()) if (it.next()==null) return true; } else { while (it.hasNext()) if (o.equals(it.next())) return true; } return false; }
public Object[] toArray() { // Estimate size of array; be prepared to see more or fewer elements Object[] r = new Object[size()]; Iteratorit = iterator(); for (int i = 0; i < r.length; i++) { if (! it.hasNext()) // fewer elements than expected return Arrays.copyOf(r, i); r[i] = it.next(); } return it.hasNext() ? finishToArray(r, it) : r; }
public boolean remove(Object o) { Iteratorit = iterator(); if (o==null) { while (it.hasNext()) { if (it.next()==null) { it.remove(); return true; } } } else { while (it.hasNext()) { if (o.equals(it.next())) { it.remove(); return true; } } } return false; }
其他的方法都是在上面几个方法上的包装处理,这里就不给大家上代码了。
这里要注意一点,Collection 接口文档建议,每个 Collection 接口的实现类 都应该提供至少 2 个标准的构造方法,描述如下:
*All general-purpose Collection implementation classes (which * typically implement Collection indirectly through one of its * subinterfaces) should provide two "standard" constructors: a void (no * arguments) constructor, which creates an empty collection, and a * constructor with a single argument of type Collection, which * creates a new collection with the same elements as its argument. In * effect, the latter constructor allows the user to copy any collection, * producing an equivalent collection of the desired implementation type. * There is no way to enforce this convention (as interfaces cannot contain * constructors) but all of the general-purpose Collection * implementations in the Java platform libraries comply.
-
AbstractList
public abstract class AbstractListextends AbstractCollection implements List { protected AbstractList() {} public boolean add(E e) {} abstract public E get(int index); public E set(int index, E element) {} public void add(int index, E element) {} public E remove(int index) {} public int indexOf(Object o) {} public int lastIndexOf(Object o) {} public void clear() {} public boolean addAll(int index, Collection c) {} public Iterator iterator() {} public ListIterator listIterator() {} public ListIterator listIterator(final int index) {} }
AbstractList 是基于以下方法进行操作的
abstract public E get(int index); public E set(int index, E element) public void add(int index, E element) public E remove(int index)
由于 AbstractList 是一个抽象类,所以 AbstractList 的子类必须重写 size 方法;get 方法也是抽象方法,也需要重写。
set、add 和 remove 方法和上面 AbstractCollection 类中的一样,默认抛出异常,如果子类容器不可以被修改,则不必重写。
AbstractList 与 AbstractCollection 的区别是 不需要子类实现 迭代器方法,AbstractList 内部已经实现了 2 个迭代器,一个是 Iterator 接口,另一个就是 ListIterator 接口。这里我们就不再代码叙述了。
-
AbstractSequentialList
public abstract class AbstractSequentialList
extends AbstractList { protected AbstractSequentialList() {} public E get(int index) {} public E set(int index, E element) {} public void add(int index, E element) {} public E remove(int index) {} public boolean addAll(int index, Collection c) {} public Iterator iterator() {} public abstract ListIterator listIterator(int index); }
AbstractSequentialList 是 AbstractList 的子类。不过这里要注意的是,AbstractSequentialList 实现了 根据索引位置的相关操作方法,即 get、set、add、remove。因为这些方法内部调用的其实是 ListIterator,而 ListIterator 已经在 AbstractList 作为一个内部类实现了,即下面代码:
//这里是 AbstractList 的内部类 private class ListItr extends Itr implements ListIterator{ ListItr(int index) { cursor = index; } public boolean hasPrevious() { return cursor != 0; } public E previous() { checkForComodification(); try { int i = cursor - 1; E previous = get(i); lastRet = cursor = i; return previous; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public int nextIndex() { return cursor; } public int previousIndex() { return cursor-1; } public void set(E e) { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.set(lastRet, e); expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } public void add(E e) { checkForComodification(); try { int i = cursor; AbstractList.this.add(i, e); lastRet = -1; cursor = i + 1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } }
终于把 LinkedList 的接口定义讲完了,我们接下来说正文。
LinkedList: 队列的实现类
我们在 LinkedList 的定义的时候讲了 继承之 AbstractSequentialList,这里我们接着补充,它实现了 Deque 接口所谓的队列就是我们常说的 先进先出(FIFO),以及后进先出(LIFO),他们的具体使用场景就是我们经常碰到的堆栈。
-
Queue
Deque 是Queue 的子类,Queue 的接口定义如下:
public interface Queueextends Collection { //尾部添加元素 boolean add(E e); //尾部添加元素 boolean offer(E e); //删除头部元素 E remove(); //删除头部元素 E poll(); //查看头部元素,即返回头部元素,不改变队列内容 E element(); //查看头部元素,即返回头部元素,不改变队列内容 E peek();
在上面的函数定义中,都有 ’重复‘ 的 方法存在,而且方法名还不一样,可能不经常使用的小伙伴们会搞混,当然,JDK 没有闲的给大家出难题记 API,这种 ’重复‘的方法是因为在处理特殊情况下的队列的时候,队列可能是空的或者是满的,(空的就是没有元素,满的是长度有限制,还不能扩容)。所以针对这种情况做了处理;当然,我们今天讲的 LinkedList 是没有长度限制的。
-
队列为空:element() 和 remove() 抛出 NoSuchElementException 异常,而peek() 和 poll() 返回null
-
队列为满:add() 抛出 IllegalStateException 异常, 而 offer() 返回 false
/** * LinkedList 作为队列使用 */ public class Test { public static void main(String[] args) { Queuequeue = new LinkedList<>(); // offer 添加元素,返回 false queue.offer(1); queue.offer(2); queue.offer(3); //peek 查看头部元素,返回 null while(queue.peek()!=null){ // poll 删除头部元素 抛出 NoSuchElementException System.out.println(queue.poll()); } // 输出 1 2 3 } }
-
Deque
双端队列接口 Deque
public interface Dequeextends Queue { void addFirst(E e); void addLast(E e); boolean offerFirst(E e); boolean offerLast(E e); E removeFirst(); E removeLast(); E pollFirst(); E pollLast(); E getFirst(); E getLast(); E peekFirst(); E peekLast(); boolean removeFirstOccurrence(Object o); boolean removeLastOccurrence(Object o); // *** Queue methods *** boolean add(E e); boolean offer(E e); E remove(); E poll(); E element(); E peek(); // *** Stack methods *** void push(E e); E pop(); // *** Collection methods *** boolean remove(Object o); boolean contains(Object o); public int size(); Iterator iterator(); Iterator descendingIterator(); }
从代码中可以看到,一个双端队列的方法竟然这么复杂。这也是在我的文章中给大家贴很多源码的原因,毕竟讲的再好不如给大家一看。
在 Deque 中,可以划分为几部分代码,一部分是他自己用的常用操作,还有一些是 stack 用的,以及 queue,和 Collection 。
这里说到了栈,我们再来稍微提一下:
-
push 表示入栈,在头部添加元素,由于栈可能是有容量限制的,所以会抛出 IllegalStateException 异常
-
pop 表示出栈,返回头部元素,并且栈中删除,如果为空,则抛出 NoSuchElementException 异常
-
在 传统的 栈中,或者说是在 Java的 Stack 类中,还有一个查看头部元素的方法,也就是 peek, 但是 在 Queue 中已经实现了,所以代码中直接把 peek 归纳到了 Queue methods 下,并不矛盾哈。如果栈为空,返回null,反之返回头部元素。
/** * LinkedList 作为栈使用的实例代码 */ public class Test { public static void main(String[] args) { Deque
stack = new LinkedList<>(); stack.push(1); stack.push(2); stack.push(3); while(stack.peek()!=null){ System.out.println(stack.pop()); } // 输出 3 2 1 } }
Deque 中还新增了个从后向前遍历的迭代方法:descendingIterator, 和普通方法一样,这里我们就不在细讲了。
构造方法
public LinkedList() { } public LinkedList(Collection c) { this(); addAll(c); }
通过上面结构的讲解,大家知道了 Collection 接口建议每个子类实现至少 2 个的构造方法,所以。LinkedList 有 2 个构造方法,因此,我们可以这么创建 LinkedList,(当然,第三方库创建的另说)
Listlist = new LinkedList<>(); List list1 = new LinkedList<>(Arrays.asList(new Integer[]{ 1,2,3}));
注意,在LinkedList 中是没有初始化容量这一概念的。
存储结构
/** * Node 是 LinkedList */ private static class Node{ E item; Node next; Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } }
Node 是 LinkedList 的内部类, Node 是一个双向链表。每个元素在内部都是单独存放的,元素之间就跟手拉手一样,链接在一起,所以叫链表。
-
item 表示当前节点
-
next 表示下一个节点
-
prev 表示前一个节点
使用方法
LinkedList 内部组成如下:
//表示长度 transient int size = 0; //指向头节点 transient Nodefirst; //指向尾节点 transient Node last;
LinkedList 内部的所有操作都是操作这 3 个变量。我们接下来具体看操作:
-
add 方法
public boolean add(E e) { linkLast(e); return true; } public void add(int index, E element) { checkPositionIndex(index); //如果 插入的下标是 size,则添加到尾部 if (index == size) linkLast(element); else //反之则,插入到 index对应节点的前面 linkBefore(element, node(index)); } public void addFirst(E e) { linkFirst(e); } public void addLast(E e) { linkLast(e); }
在上面的几个 add 方法中, 都调用了 linkXXX 系列方法。这里我们以 linkLast 为例,linkBefore、linkLast linkFirst原理相同,我们不再叙述。
void linkLast(E e) { final Nodel = last; final Node newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
主要步骤:
-
定位到尾节点 last
-
创建一个新的 Node 节点
-
修改尾节点
-
修改前后链接,如果原链表是空的,则让头部节点指向新的 Node 节点
-
增加链表大小 size
-
增加修改次数 modCount (modCount 我们在上一篇 中已经讲过,判断结构性修改)
这里我们使用表格来给大家展示,图文结合理解的更加清楚
public static void main(String[] args) { Listlist = new LinkedList<>(); list.add("a"); list.add("b"); }
这是初始化的时候,内部结构,即 new LinkedList<>(); first 和 last 指向的都是 null, size 为 0
添加第一个元素后 :list.add(“a”); first 和 last 指向同一个 node 元素
添加第二个元素后:first指向第一个 node -- a, last 指向第二个 node -- b
通过上面的图像,我们可以看出,LinkedList 是按需分配的。
public boolean addAll(Collection c) { return addAll(size, c); } public boolean addAll(int index, Collection c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Nodepred, 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 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; }
至于 2个 addAll 方法,原理都一样,只是 循环调用构建 prev 和 next。
上面的 checkPositionIndex 是检查下标,代码很简单:
private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }
-
get 方法
public E get(int index) { checkElementIndex(index); return node(index).item; } public E getFirst() { final Nodef = first; if (f == null) throw new NoSuchElementException(); return f.item; } public E getLast() { final Node l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
getFirst 和 getLast 我们不再叙述,看一下 get方法。get 方法是根据 索引去当前节点,我们来看一下如何获取索引值:
Nodenode(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { //前半部分开始找 Node x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { //后半部分开始找 Node x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
这里有个 位运算:size >> 1 ,也就是 size/2。如果索引位置在前半部分,则从头开始找,如果索引位置在后半部分,则从尾开始找。
-
remove方法
public E remove() { //移除头部节点 return removeFirst(); } public E remove(int index) { checkElementIndex(index); //通过 node(int index)计算出 下标所在位置的节点,然后调用 unlink 删除节点 return unlink(node(index)); } // 由于LinkedList 允许存入 null,所以先判断 节点是 null的 public boolean remove(Object o) { if (o == null) { for (Node
x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } //移除头部节点 public E removeFirst() { final Node f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } //移除首次出现的指定元素,这里是正序迭代 public boolean removeFirstOccurrence(Object o) { return remove(o); } //移除尾节点 public E removeLast() { final Node l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } //移除最后出现的指定元素,这里是倒序迭代 public boolean removeLastOccurrence(Object o) { if (o == null) { for (Node x = last; x != null; x = x.prev) { if (x.item == null) { unlink(x); return true; } } } else { for (Node x = last; x != null; x = x.prev) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
其实上述几个方法的核心就是 unlink(),我们继续看:
E unlink(Nodex) { // assert x != null; final E element = x.item; final Node next = x.next; final Node 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 的核心思路就是把当前节点的前驱和后驱直接连起来,然后自身在没有引用的情况下被GC回收:
-
x 的前驱指向后驱,如果 前驱 prev 是null,那么证明 x 是头部节点,修改头节点 first 指向后驱 next
-
x 的后驱指向 前驱,如果后驱 next 是null,那么证明 x 是尾部节点,修改尾节点 last 指向前驱 prev
如图,假如我们删除 b 节点,也就是 remove(1).
-
其他方法
public boolean offer(E e) { return add(e); } public boolean offerFirst(E e) { addFirst(e); return true; } public boolean offerLast(E e) { addLast(e); return true; } public E peekFirst() { final Node
f = first; return (f == null) ? null : f.item; } public E peekLast() { final Node l = last; return (l == null) ? null : l.item; } public E pollFirst() { final Node f = first; return (f == null) ? null : unlinkFirst(f); } public E pollLast() { final Node l = last; return (l == null) ? null : unlinkLast(l); } public void push(E e) { addFirst(e); } public E pop() { return removeFirst(); } public void push(E e) { addFirst(e); } 上面的一些队列方法,核心还是我们之前提到的,这里就不在叙述了。
总结
最后我们再来总结一波:
-
LinkedList 是List 的一个子类,但它同时也实现了 Queue,可以用作队列。
-
没有初始容量概念,按需分配空间
-
查找效率低,查找 某元素时间复杂度为 O(N), 索引位置查找为 O(N/2)
-
头部、尾部插入删除效率高,为 O(1)
-
中间插入删除效率较低,为 O(N),注意,这里的 O(N) 是花费时间在定位上,修改元素操作还是 O(1)