Java容器深度总结:LinkedList

唯有心静,身外的繁华才不至于扭曲和浮躁,才能倾听到内心真实的声音。

1.LinkedList概述

LinkedList:JDK1.2的时候添加的集合类,底层是使用双向链表实现的线性表,可用作队列、双端队列,也可以模拟栈空间的储存。

LinkedList类图:
在这里插入图片描述
LinkedList定义:

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

可以看出来:

  • LinkedList< E > :LinkedList支持泛型。
  • 继承AbstractSequentialList< E >:该类是链表实现的线性表的统一父类,AbstractSequentialList继承了AbstractList,但该类只支持按次序访问。
  • 实现了List< E >:因此也可以使用get(index)访问元素,但是效率较低,因为该索引由LinkedList内部维护,需要从链表头或尾顺序遍历。
  • 实现了 Deque< E >:表明LinkedList支持双端队列(Deque)的相关操作。
  • 实现了Cloneable、Serializable接口:表明LinkedList可以通过调用clone()方法进行克隆,并且LinkedList是支持序列化的。

注意:LinkedList没有实现RandomAccess接口,因此它不具备快速随机访问的能力。

Tip:

LinkedList在不同JDK版本有细微差异:

  • JDK1.5时LinkedList直接实现Queue(队列)接口;JDK1.6开始改为实现Deque(双端队列)接口。
  • JDK1.6时,LinkedList由带头结点的双向循环链表实现;JDK1.7开始由不带头结点的普通双向链表实现。

文章主要分析JDK1.8中LinkedList的实现,旧版本会有所提及。

2.LinkedList数据结构

JDK1.6使用的是一个带有头结点的双向循环链表,头结点不存储实际数据,因此只能从头结点开始遍历查找。

在这里插入图片描述
JDK1.7开始,使用的是不带头结点的普通的双向链表,增加两个节点指针first、last分别指向首尾节点。因此可以从头或尾结点开始遍历查找。

在这里插入图片描述

3.Node结点

LinkedList 不仅要保存相应的元素,还需要记录这个元素的前驱和后继,因此LinkedList使用一个对象Node来作为元素结点。

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;
        }
}

注意:JDK1.6中,Node被叫做Entry,两者只是名字不同。

4.成员变量和构造函数

JDK1.6:

private transient Entry<E> header = new Entry<E>(null, null, null);

private transient int size = 0; // 元素个数

public LinkedList() {
     header.next = header.previous = header;
}
 
public LinkedList(Collection<? extends E> c) {
     this();
     addAll(c);
}

JDK1.6时,header为头结点指针,指向一个空的Entry结点,无参构造方法中,让这个空Entry结点的前驱后继都指向自己。带参构造传入一个Collection集合,先调用无参构造初始化双线循环链表,然后调用addAll()将元素添加到链表中。

JDK1.7开始:

transient int size = 0; //元素个数
transient Node<E> first; // 首元素结点指针
transient Node<E> last; // 尾元素结点指针

public LinkedList() {
}
    
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

JDK1.7开始,无参构造为空实现,即首元素指针(first)和尾元素结点指针(last)都为null。带参构造同JDK1.6一样。

5.操作链表的底层方法

LinkedList提供了一系列直接操作链表的方法,很多方法都是基于这些底层方法实现的。

5.1 linkFirst(E e)

在表头添加指定元素e。

private void linkFirst(E e) {
   
    //使节点f指向原来的头结点
    final Node<E> f = first;
    
    //新建节点newNode,节点的前驱指针指向null,后指针原来的头节点
    final Node<E> newNode = new Node<>(null, e, f);
    
    //头指针指向新的头节点newNode 
    first = newNode;
    
    //如果原来的头结点为null,更新尾指针,否则使原来的头结点f的前置指针指向新的头结点newNode
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

在表头添加指定元素,只需要将新创建的结点(newNode)插在first指向的首结点之前,然后更新first指向新插入的结点(newNode)即可。

但是需要注意的是:

  • 如果向空链表插入新结点,那么还需让尾元素指针(last)也指向这个新结点,因为只有一个结点时,该结点既是首结点又是尾结点。
  • 不是空链表时,还需要将原来的首结点(f)的前驱指向新结点(newNode)。

最后再让size自增维护元素个数,modCount自增,记录一次结构修改。

5.2 linkLast(E e)

在表尾添加指定元素e。

void linkLast(E e) {
        
     //使节点l指向原来的尾结点
    final Node<E> l = last;
    
    //新建节点newNode,节点的前指针指向l,后指针为null
    final Node<E> newNode = new Node<>(l, e, null);
    
    //尾指针指向新的头节点newNode
    last = newNode;
    
    //如果原来的尾结点为null,更新头指针,否则使原来的尾结点l的后置指针指向新的头结点newNode
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

在尾部插入新结点的逻辑与linkFirst()相似。

5.3 linkBefore(E e, Node< E > succ)

在指定结点succ之前插入指定元素e。

void linkBefore(E e, Node<E> succ) {
   
    // assert succ != null;
    //获得指定节点的前驱
    final Node<E> pred = succ.prev;
    
    //新建节点newNode,前置指针指向pred,后置指针指向succ
    final Node<E> newNode = new Node<>(pred, e, succ);
   
    //succ的前置指针指向newTouch
    succ.prev = newNode;
   
    //如果指定节点的前驱为null,将newTouch设为头节点。否则更新pred的后置节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

在指定结点前插入指定元素也比较简单,只需要将新节点(newNode)先插入到指定结点(succ)和 指定结点(succ)的前一个结点之间。

插入之后,修改指定结点(succ)前驱为newNode。由于pred可能为null(succ本身就是首结点时),所以若pred为空,就要让首元素指针(first)指向新结点(newNode),否则就让pred的后继为新结点(newNode)。

最后再让size自增维护元素个数,modCount自增,记录一次结构修改。

5.4 unlinkFirst( Node< E > f)

删除并返回头结点f。

private E unlinkFirst( Node<E> f) {
    
    // assert f == first && f != null;
    // 保存头结点的值,用于返回
    final E element = f.item;
    
    // 保存头结点指向的下个节点
    final Node<E> next = f.next;
    
    //头结点的值置为null
    f.item = null;
    //头结点的后置指针指向null,帮助垃圾回收
    f.next = null; // help GC
    
    //将头结点置为next
    first = next;
    
    //如果next为null,将尾节点置为null,否则将next的后置指针指向null
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    //返回被删除的头结点的值
    return element;
}

删除首结点时,需要将首元素结点(first)指向原首结点(f)的后继即可。其中需要注意删除f结点后成为空链表的情况。

5.5 unlinkLast(Node< E > l)

删除并返回尾结点 l 。

private E unlinkLast(Node<E> l) {
    
    // assert l == last && l != null;
    // 保存尾节点的值,用于返回
    final E element = l.item;
    
    //获取新的尾节点prev
    final Node<E> prev = l.prev;
   
    //旧尾节点的值置为null
    l.item = null;
    //旧尾节点的后置指针指向null,帮助垃圾回收
    l.prev = null; // help GC
    
    //将新的尾节点置为prev
    last = prev;
    
    //如果新的尾节点为null,头结点置为null,否则将新的尾节点的后置指针指向null
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    //返回被删除的尾节点的值
    return element;
}

删除尾结点逻辑与unlinkFirst()相似。

5.6 unlink(Node< E > x)

删除指定结点x,返回被删除结点中保存的元素。

E unlink(Node<E> x) {
   
    // assert x != null;
    // 保存指定节点的值
    final E element = x.item;
   
    // 获取指定节点的下个节点next
    final Node<E> next = x.next;
  
    // 获取指定节点的前一个节点prev
    final Node<E> prev = x.prev;
   
    //如果prev为null,那么next为新的头结点,
    //否则将prev的后置指针指向next,x的前置指针指向null
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
   
    //如果next为null,那么prev为新的尾结点,
    //否则将next的前置指针指向prev,x的后置指针指向null
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    
    //x的值置为null
    x.item = null;
    size--;
    modCount++;
    //返回被删除的节点的值
    return element;
}

删除指定结点时,需要注意该结点的前驱和后继为null的情况。

5.7 node(int index)

获取索引处结点。

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 / 2 时,从首元素结点开始遍历;
  • 否则从尾元素结点开始遍历;

从中可以发现,带有头结点指针和尾结点指针的双向链表在查询时,会根据下标index在链表中的相对位置来进行遍历,一定程度上加快了查询速度。

6.常用方法
6.1 列表方法
签名描述复杂度
E get(int index)获取指定位置的元素O(n)
E set(int index, E element)设置指定位置的元素值O(n)
void add(int index, E element)在指定位置添加元素O(n)
E remove(int index)删除指定位置的元素O(n)
E remove(Object o)移除链表中第一次出现的指定元素O(n)
int indexOf(Object o)查询指定元素首次的位置O(n)
boolean contains(Object o)是否包含某个元素O(n)
int size()返回链表中的元素个数O(1)
boolean isEmpty()链表是否为空O(1)
void clear()从链表中移除所有元素O(n)
6.2 队列(Queue)操作
签名描述复杂度
E peek()返回头节点(队首)元素O(1)
E element()返回头节点(队首)元素O(1)
E poll()返回并删除头节点(出队 )O(1)
E remove()返回并删除头节点(出队 )O(1)
boolean offer(E e)尾结点添加元素(入队)O(1)
6.3 双端队列(Deque)操作
签名描述复杂度
E peekFirst()返回队列的头元素O(1)
E peekLast()返回队列的尾元素O(1)
boolean offerFirst(E e)插入指定元素到队列头部O(1)
boolean offerLast(E e)插入指定元素到队列尾部O(1)
E pollFirst()队首出队O(1)
E pollLast()队尾出队O(1)
void push(E e)入栈O(1)
E pop()出栈O(1)
7.迭代器
7.1 ListIterator迭代器

LinkedList覆盖了listIterator()方法,返回了ListItr类的实例对象。

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

ListItr是LinkedList的内部类,实现了ListIterator接口:

private class ListItr implements ListIterator<E>

因此ListItr具备正向、反向迭代的能力。ListItr是快速失败(fail-fast)的。

7.2 Iterator迭代器

LinkedList没有覆盖iterator()方法,因此继承了父类AbstractSequentialList中的iterator()方法:

// AbstractSequentialList类中的iterator方法
public Iterator<E> iterator() {
    return listIterator();
}

AbstractSequentialList调用了父类AbstractList中的listIterator()方法,返回ListIterator的实现类,这里发生了多态。

// AbstractList类中的 listIterator方法
public ListIterator<E> listIterator() {
    return listIterator(0);
}

LinkedList重写了listIterator()方法,因此实际会执行LinkedList中的listIterator()方法。

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

因此,LinkedLis.iterator()方法实质上也会返回一个ListItr类的对象,只不过静态类型为Iterator,只能调用Iterator中定义的方法,因此只能正向迭代。

7.3 DescendingIterator迭代器

LinkedList.descendingIterator()方法可以返回一个DescendingIterator迭代器对象。

public Iterator<E> descendingIterator() {
    return new DescendingIterator();
}

DescendingIterator迭代器实现了Iterator接口,因此只能正向迭代。该迭代器通过封装ListItr迭代器实现。

private class DescendingIterator implements Iterator<E> {
    
    private final ListItr itr = new ListItr(size());
    public boolean hasNext() {
        return itr.hasPrevious();
    }
    public E next() {
        return itr.previous();
    }
    public void remove() {
        itr.remove();
    }
}
8.快速失败(fail-fast)

LinkedList像ArrayList一样,具有快速失败(fail-fast)机制,其实现原理与ArrayList相同,不再赘述。

9.clone()机制

LinkedList的clone()方法返回一个新的LinkedList,并且结点Node是深克隆的,但对于Node中保存的元素依然是浅克隆。

private LinkedList<E> superClone() {
    try {
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}

public Object clone() {
    LinkedList<E> clone = superClone();

    // 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;
}
10.序列化

LinkedList实现了writeObject()和readObject()方法,在序列化时不会序列化整个链表及其结点,只会序列化链表的元素个数和每个结点中保存的元素。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out size
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (Node<E> x = first; x != null; x = x.next)
        s.writeObject(x.item);
}
    
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
   
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());
}
11.LinkedList与ArrayList
  • LinkedList在进行插入、删除等操作时所花的开销都是固定的;而ArrayList只在数组末尾进行插入、删除很快,在数组中间插入、删除时会有元素移动,因此效率低。
  • LinkedList虽然提供了优化的查询方法,但效率依然较低(需要移动指针);ArrayList可以通过索引快速获取元素(随机访问快)。
  • ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

总的来说:

  • ArrayList使用在查询比较多,但是插入和删除比较少的情况;
  • LinkedList用在查询比较少,而插入和删除比较多的情况;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值