Java中的LinkedList底层源码分析
作者:爱喝咖啡的程序员
一. 基本原理和优缺点
优点:
1.底层基于双向链表,往LinkedList中间插入元素时,不需要移动大量的元素,只需要修改前后节点的指针,速度快。
2.适合频繁、大量的插入元素,不会导致频繁的扩容和拷贝元素。插入元素,只不过就是把新的元素挂到旧元素下面。
缺点:
1.不适合读取随机位置的元素,比如list.get(10),因为需要遍历链表,直到找到这个位置上的元素为止。
二. 源码分析
2.1 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; else l.next = newNode; size++; modCount++; }
2.2 node
在双向链表中,找到目标下标对应的节点。这里可以学习链表的遍历方式。
Node<E> node(int index) { // assert isElementIndex(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; } }
首先,通过index < (size >> 1),判断待寻找的节点在双向链表的前半部分,还是后半部分。
如果是前半部分,则会从双向链表的头节点开始遍历,通过节点的next指针,不断的向后寻找节点。→
如果是后半部分,则会从双向链表的尾节点开始遍历,通过节点的prev指针,不断的向前寻找节点。←
2.3 add(int index, E element)
在指定元素的前面,插入一个元素。比如现在想要在队尾插入一个元素。
void linkBefore(E e, Node<E> succ) { // assert succ != null; 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++; }
succ指向队尾元素,succ.prev表示队尾节点前面的那个节点(倒数第二个节点)。
首先,创建一个新节点,让新节点的prev指针指向倒数第二个节点,新节点的next指针指向队尾节点。
接着,让队尾节点指的prev指向新节点。
最后,把倒数第二个节点的next指针指向新节点。
2.4 get
获取一个随机位置的节点中的值。
public E get(int index) { checkElementIndex(index); return node(index).item; }
随机读取是ArrayList的强项, 因为ArrayList底层基于数组实现,通过下标能快速找到对应的内存地址,接着直接读取内存地址中的值。
随机读取是LinkedList弱项,LinkedList底层基于双向链表实现,它没办法通过下标,直接找到内存地址,必须从头、尾节点开始,借助节点的prev和next指针,不断的向前或向后寻找,直到找到元素为止。
node()方法的代码之前已经学习过了,先大致的判断目标节点距离头、尾节点,哪个节点更近,尽量的减少查询的次数,接着就是借助头尾节点,不断的向前或向后找,比如从头节点,不断的向后找。
2.5 getFirst
返回头节点的值。如果头节点为空,则抛出异常。
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; }
2.6 peek
返回头节点的值,头节点不需要出队。
peek与getFirst的区别:如果不存在头节点,peek会返回null,而getFirst会直接报错。
public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; }
2.7 getLast
返回尾部节点的值。
public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
2.8 removeLast
删除队尾节点。
public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); }
return xprivate 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; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; };
通过last指针找到队尾元素,断开队尾元素与倒数第二个元素的指针指向,last指针指向倒数第二个元素,作为新的队尾元素。
此时,原队尾节点的next指针等于null,item等于null,prev等于null,且没有被任何节点指向,接着就靠JVM来进行垃圾回收了。
2.9 removeFirst
删除队头节点。
public E removeFirst() { 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; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
把队列的头节点与第二个节点之前的指针指向全部断开,让JVM来回收头节点。
接着,让first指针原队列的第二个节点,作为队列新的头结点。
2.10 remove(int index)
删除指定下标的节点。
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); }
E unlink(Node<E> x) { // assert x != null; 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; }
首先,通过node方法,遍历链表,找到待删除的节点。
接着,解除待删除节点,对于左右两边节点的所有指针指向。让左右两边的节点的next和prev指针互相指向。
最后,由JVM回收这个没有任何人指向的节点。
三. 总结
LinkedList是一个基于双向链表实现的数据结构,对于队头和队尾节点来说,无论是插入、删除还是读取节点的值,其实都是很轻松的。并且,默认从队尾插入节点,从队头获取节点,所以LinkedList天然就可以作为队列来使用。
由于基于双向链表实现,所以无论你怎么插入数据,LinkedList的性能都很不错,不用担心扩容,移动大量元素等问题,性能上很好。
但是呢,在链表的中间插入元素,比在队头和队尾插入元素的性能要差一些,这是因为队头和队尾分别有first和last指针指向着它们,如果要在链表的中间指定位置插入元素,首先要遍历链表,找到目标元素,然后才能修改左右两边节点的指针,插入节点。
此外,如果要随机获取某个位置的元素,尤其是链表内节点的数量很多的时候,由于需要遍历链表,所以性能比较差。
到此这篇关于Java中的LinkedList底层源码分析的文章就介绍到这了,更多相关LinkedList底层源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!