Java集合:LinkedList详解

mac2024-03-31  28

说明

本文主要参考自以下文章,包含内容的转载,在此表示感谢:

Java集合:LinkedList详解java集合之LinkedList详解

文章目录

说明概述1. LinkedList相关概念2. LinkedList相关源码解析2.1 LinkedList的实现2.1.1 基础属性2.1.2 构造方法2.1.3 fail-fast机制 2.2 add()2.2.1 链表尾部添加2.2.2 指定索引添加 2.3 get()2.3.1 node(int index) 2.4 set()2.5 remove()2.5.1 移除指定元素2.5.2 移除指定索引上的元素 2.6 clear()2.7 link相关方法2.7.1 linkLast()2.7.2 linkBefore()2.7.3 unlink() 2.8 clone()2.9 toArray()2.9.1 转换为普通数组2.9.2 转换为指定类型的数组 3. Queue相关操作3.1 基础操作3.2 Deque 双向队列操作 4. LinkedList迭代器4.1 指定索引开始迭代4.2 逆向迭代器spliterator() ArrayList和LinkedList比较二者如何选择?总结

概述

介绍数据结构–LinkedList的相关概念及操作。

1. LinkedList相关概念

LinkedList底层为双链表,维护的是一个first和last指针,而每个节点有item自身、prev和next两个节点来维护双链表的关系,其他的功能都是围绕双链表来进行的。由于基于链表,LinkedList没有固定容量,不需要进行扩容LinkedList和 ArrayList 一样,不是同步容器。但是相对需要更多的内存,因为LinkedList 每个节点中需要多存储前后节点的信息,占用空间更多些。元素是有序的,输出顺序与输入顺序一致允许元素为 null

LinkedList 和 ArrayList 一样,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下:

List list = Collections.synchronizedList(new LinkedList(...));

此时list是线程安全类,自身提供的方法也是线程安全的。当然list进行其他非原子操作仍需自己同步。



2. LinkedList相关源码解析

2.1 LinkedList的实现

LinkedList继承了AbstractSequentialList并实现了List接口,Deque接口,Cloneable接口,Serializable接口,因此它有List的特性(add,remove,etc.)支持队列操作,可复制,可序列化。

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList底层基于双向链表,并且使用了size属性来记录链表的长度(结点的数量)。其基本的数据结构如下:

2.1.1 基础属性
transient int size = 0; // 节点数量 transient Node<E> first; // 第一个节点(头结点) transient Node<E> last; // 最后一个节点(尾节点) private static class Node<E> { // Node的数据结构 : 双向结点 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; } }
2.1.2 构造方法

LinkedList提供了三种构造器

方法 说明 LinkedList()构造一个空链表。LinkedList(Collection c)构造一个包含指定 collection 的元素的列表。这些元素是按照该 collection 的迭代器返回它们的顺序排列的。

LinkedList(Collection c)调用了无参构造方法,并调用了addAll()。

addAll()的主要作用是:将指定集合的所有元素添加到当前列表的尾部,按照指定集合的顺序插入,如果在操作正在进行时修改指定的集合,则此操作的行为是不确定的。此处说明了LinkedList不是线程安全的。

/** * Constructs an empty list. */ public LinkedList() { } /** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
2.1.3 fail-fast机制

类似HashMap、ArrayList等,LinkedList也维护了modCount变量来实现fail-fast机制,其记录了数组的修改次数,在LinkedList的所有涉及结构变化的方法中都增加modCount的值。 该变量迭代器等方面体现。

//检查链表是否修改,根据expectedModCount和modCount判断 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }

因此在使用迭代器迭代过程中,不允许对链表结构做修改(如插入新节点),否则会抛出异常 java.util.ConcurrentModificationException。


2.2 add()

2.2.1 链表尾部添加

add(E e):调用linkLast方法将元素添加到尾部(linkLast方法详解见下文)

public boolean add(E e) { linkLast(e); // 调用linkLast方法, 将节点添加到尾部 return true; }
2.2.2 指定索引添加

add(int index, E element):

检查index是否越界比较index与size,如果index==size,则代表插入位置为链表尾部,调用linkLast()(linkLast方法详解见下文),否则调用linkBefore()(LinkBefore方法详解见下文) public void add(int index, E element) { // 在index位置插入节点,节点值为element checkPositionIndex(index); if (index == size) linkLast(element);// 如果索引为size,即将element插入链表尾部 else // 否则,将element插入原index位置节点的前面,即:将element插入index位置,将原index位置节点移到index+1的位置 linkBefore(element, node(index)); // 将element插入index位置 }


2.3 get()

校验index是否越界调用node方法寻找目标节点,并返回目标节点的item(node方法详解见下文) public E get(int index) { checkElementIndex(index); // 校验index是否越界 return node(index).item; // 根据index, 调用node方法寻找目标节点,返回目标节点的item }
2.3.1 node(int index)
该方法获得指定位置index的节点,其实现虽然也是遍历链表,但由于该链表是双向链表,因此支持双向查找。查找前会根据指定位置index判断是在链表的前半段还是后半段,从而决定是从前往后找或是从后往前找,提升查找效率。 /** * Returns the (non-null) Node at the specified element index. */ //返回指定index位置的节点 Node<E> node(int index) { // assert isElementIndex(index); //首先去比较index和size >> 1(也就是size的一半),如果比中间数小则从链表头找,否则从尾找 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; } }


2.4 set()

检查index是否越界调用node()寻找目标节点(node方法详解见上文)将目标节点的item属性设为element public E set(int index, E element) { // 替换index位置节点的值为element checkElementIndex(index); // 检查index是否越界 Node<E> x = node(index); // 根据index, 调用node方法寻找到目标节点 E oldVal = x.item; // 节点的原值 x.item = element; // 将节点的item属性设为element return oldVal; //返回节点原值 }


2.5 remove()

2.5.1 移除指定元素

remove(Object o):

判断o是否为null,如果o为null,则遍历链表寻找item属性为空的节点,并调用unlink方法将该节点移除(unlink方法详解见下文)如果o不为null, 则遍历链表寻找item属性跟o相同的节点,并调用unlink方法将该节点移除(unlink方法详解见下文) public boolean remove(Object o) { if (o == null) { // 如果o为空, 则遍历链表寻找item属性为空的节点, 并调用unlink方法将该节点移除 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { // 如果o不为空, 则遍历链表寻找item属性跟o相同的节点, 并调用unlink方法将该节点移除 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
2.5.2 移除指定索引上的元素

remove(int index):

检查index是否越界调用unlink方法,移除index位置的节点(unlink方法详解见下文) public E remove(int index) { // 移除index位置的节点 checkElementIndex(index); // 检查index是否越界 return unlink(node(index)); // 移除index位置的节点 }


2.6 clear()

清除链表的所有节点:

从first节点开始,遍历将所有节点的属性清空将first节点和last节点设为null public void clear() { // 清除链表的所有节点 // Clearing all of the links between nodes is "unnecessary", but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node<E> x = first; x != null; ) { // 从头结点开始遍历将所有节点的属性清空 Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; // 将头结点和尾节点设为null size = 0; modCount++; }


2.7 link相关方法

2.7.1 linkLast()
拿到当前的尾节点l节点使用e创建一个新的节点newNode,prev属性为l节点,next属性为null将当前尾节点设置为上面新创建的节点newNode如果l节点为空则代表当前链表为空, 将newNode设置为头结点,否则将l节点的next属性设置为newNode void linkLast(E e) { // 将e放到链表的最后一个节点 final Node<E> l = last; // 拿到当前的尾节点l节点 final Node<E> newNode = new Node<>(l, e, null); // 使用e创建一个新的节点newNode, prev属性为l节点, next属性为null last = newNode; // 将当前尾节点设置为上面新创建的节点newNode if (l == null) // 如果l节点为空则代表当前链表为空, 将newNode设置为头结点 first = newNode; else // 否则将l节点的next属性设置为newNode l.next = newNode; size++; modCount++; }
2.7.2 linkBefore()
拿到succ节点的prev节点使用e创建一个新的节点newNode,其中prev属性为pred节点,next属性为succ节点将succ节点的prev属性设置为newNode如果pred节点为null,则代表succ节点为头结点,要把e插入succ前面,因此将first设置为newNode,否则将pred节点的next属性设为newNode void linkBefore(E e, Node<E> succ) { // 将e插入succ节点前面 // assert succ != null; final Node<E> pred = succ.prev; // 拿到succ节点的prev节点 final Node<E> newNode = new Node<>(pred, e, succ); // 使用e创建一个新的节点newNode,其中prev属性为pred节点,next属性为succ节点 succ.prev = newNode; // 将succ节点的prev属性设置为newNode if (pred == null) // 如果pred节点为null,则代表succ节点为头结点,要把e插入succ前面,因此将first设置为newNode first = newNode; else // 否则将pred节点的next属性设为newNode pred.next = newNode; size++; modCount++; }
2.7.3 unlink()
定义element为x节点的值,next为x节点的下一个节点,prev为x节点的上一个节点如果prev为空,则代表x节点为头结点,则将first指向next即可;否则,x节点不为头结点,将prev节点的next属性指向x节点的next属性,并将x的prev属性清空如果next为空,则代表x节点为尾节点,则将last指向prev即可;否则,x节点不为尾节点,将next节点的prev属性指向x节点的prev属性,并将x的next属性清空将x的item属性清空,以便垃圾收集器回收x对象 E unlink(Node<E> x) { // 移除链表上的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) { // 如果prev为空,则代表x节点为头结点,则将first指向next即可 first = next; } else { // 否则,x节点不为头结点, prev.next = next; // 将prev节点的next属性指向x节点的next属性 x.prev = null; // 将x的prev属性清空 } if (next == null) { // 如果next为空,则代表x节点为尾节点,则将last指向prev即可 last = prev; } else { // 否则,x节点不为尾节点 next.prev = prev; // 将next节点的prev属性指向x节点的prev属性 x.next = null; // 将x的next属性清空 } x.item = null; // 将x的值清空,以便垃圾收集器回收x对象 size--; modCount++; return element; }


2.8 clone()

返回副本,浅拷贝,与ArrayList.clone()相似。

//返回副本,浅拷贝,与ArrayList.clone()相似 public Object clone() { LinkedList<E> clone = superClone(); //将clone构造成一个空的双向循环链表 // Put clone into "virgin" state clone.first = clone.last = null; clone.size = 0; clone.modCount = 0; // Initialize clone with our elements for (Node<E> x = first; x != null; x = x.next) clone.add(x.item); //浅拷贝,节点还是同一份引用 return clone; }


2.9 toArray()

链表转换为数组主要是进行拷贝工作,跟上述clone()方法类似,同样也是浅拷贝。

2.9.1 转换为普通数组
//返回一个包含此列表中所有元素的数组 public Object[] toArray() { Object[] result = new Object[size]; int i = 0; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; return result; }
2.9.2 转换为指定类型的数组

返回一个数组,使用运行时确定类型,该数组包含在这个列表中的所有元素(从第一到最后一个元素):

调用时,需要先传入一个想要的类型的空数组,称为参数数组。如果参数数组容量比链表节点数少,重新申请一个容量足够的数组(大小为size),再进行拷贝;否则覆盖参数数组前size位,且第size位赋null,剩余不变。返回此参数数组。 //返回一个数组,使用运行时确定类型,该数组包含在这个列表中的所有元素(从第一到最后一个元素) //如果参数数组容量比链表节点数少,则返回链表数组;否则覆盖参数数组前size位,且第size位赋null,剩余不变。 @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { //如果参数数组容量不够,则重新申请容量足够的数组 if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); int i = 0; Object[] result = a; //遍历依次覆盖 for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; if (a.length > size) a[size] = null; return a; }

3. Queue相关操作

3.1 基础操作

由于LinkedList实现了Deque接口,而Deque继承了Queue,因此LinkedList也可以进行队列操作,包括:

// 队列操作,获取表头节点的值,表头为空返回null public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; } // 队列操作,获取表头节点的值,表头为空抛出异常 public E element() { return getFirst(); } // 队列操作,获取表头节点的值,并删除表头节点,表头为空返回null public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } // 队列操作,获取表头节点的值,并删除表头节点,表头为空抛出异常 public E remove() { return removeFirst(); } // 队列操作,将指定的元素添加为此列表的尾部(最后一个元素)。 public boolean offer(E e) { return add(e); }

3.2 Deque 双向队列操作

由于LinkedList实现了Deque接口,因此可用于双向队列

// 双向队列操作,链表首部插入新节点 public boolean offerFirst(E e) { addFirst(e); return true; } // 双向队列操作,链表尾部插入新节点 public boolean offerLast(E e) { addLast(e); return true; } // 双向队列操作,获取链表头节点值 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; } // 双向队列操作,获取表头节点的值,并删除表头节点,表头为空返回null public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } // 双向队列操作,获取表尾节点的值,并删除表尾节点,表尾为空返回null public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); }

4. LinkedList迭代器

4.1 指定索引开始迭代

其中ListItr是LinkedList的一个内部类,实现了ListIterator接口,是个支持双向的迭代器。

listIterator(int index):

// 返回从指定位置开始的ListIterator迭代器 public ListIterator<E> listIterator(int index) { checkPositionIndex(index); //检查位置合法性 return new ListItr(index); }

4.2 逆向迭代器

利用上面实现的双向迭代器类ListItr,可轻易的实现逆向的迭代器。

descendingIterator():

// 返回一个迭代器在此双端队列以逆向顺序的元素 public Iterator<E> descendingIterator() { return new DescendingIterator(); } // DescendingIterator的实现,从后往前的迭代 private class DescendingIterator implements Iterator<E> { private final ListItr itr = new ListItr(size()); //获得链表尾部的ListItr public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); } }

spliterator()

java8新增方法,类似Iiterator, 可以理解为 Iterator 的 Split 版本:

使用 Iterator 的时候,我们可以顺序地遍历容器中的元素,使用 Spliterator 的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。使用 Spliterator 每次可以处理某个元素集合中的一个元素 — 不是从 Spliterator 中获取元素,而是使用 tryAdvance() 或 forEachRemaining() 方法对元素应用操作。但 Spliterator 还可以用于估计其中保存的元素数量,而且还可以像细胞分裂一样变为一分为二。这些新增加的能力让流并行处理代码可以很方便地将工作分布到多个可用线程上完成。


ArrayList和LinkedList比较

主要是数组和链表的区别,访问、插入、删除等方面的区别。在这里面,涉及的是寻址开销、复制开销、内存占用等方面的问题

LinkedList详解可以看我的另一篇文章:Java集合:ArrayList详解

ArrayList底层基于动态数组实现(扩容1.5倍,拷贝数组),LinkedList底层基于链表实现对于随机访问(get/set方法),ArrayList通过index直接定位到数组对应位置的节点,而LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList对于插入和删除(add/remove方法),ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的next或prev属性即可,因此在效率上LinkedList优于ArrayList。(然而对于插入和删除来说,首先是需要进行查找操作,大体上相当于随机访问,那么如果考虑到这部分,LinkedList未必就优于ArrayList。因此,在选择上,如果你是插入,需要综合考虑再选择)从内存的角度来说,LinkedList更适用于存储较少元素。因为LinkedList里面不仅维护了待插入的元素,还维护了Node的前置Node和后继Node,如果一个LinkedList中的Node非常多,那么LinkedList将比ArrayList更耗费一些内存;并且在访问上,太多的元素会导致查找效率低下。

二者如何选择?

元素较少,优先LinkedList;插入和删除较频繁,优先LinkedList;访问较为频繁,优先ArrayList;访问、插入、删除均频繁,取决于元素数量和实际情况;内存角度来说,ArrayList在插入删除中移动元素,那么会有大量的复制内存开销;LinkedList则需要保存很对的prev和next,较为耗费内存;总体来说,需要考虑实际情况。例如插入来说,假设一直是尾插,那么实际上ArrayList的效率并不见得低于LinkedList,反而LinkedList一直需要修改并保存新的结点指向,效率低些。


总结

介绍了LinkedList的基本原理,并介绍了其构造方法和提供的操作,最后对比了ArrayList。

最新回复(0)