LinkedList源码解析

LinkedList源码解析

1.概览:

1.1底层结构:

LinkedList底层使用的双向链表结构,使用 Node 存储链表节点信息,除了结点的值以外,包括前驱结点和后继结点。每个链表都维护了一个头指针first和一个尾指针last。这意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。

1.2 属性:


  public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
        // 实际元素个数
        transient int size = 0;
        // 头结点
        transient Node<E> first;
        // 尾结点
        transient Node<E> last;
    }


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

2.重要方法解析:

2.1构造器

//无参构造器
public LinkedList() {
}

//有参构造器,按照集合初始化数据,调用addAll()方法将集合中的元素按顺序添加到链表中。
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

2.2 新增/添加 方法

addLast(E e)

尾插 相当于add(E e)

public boolean add(E e) {
	//尾插,调用linkLast()方法,成功返回true
    linkLast(e);
    return true;
}

public void addLast(E e) {
        linkLast(e);
}

 void linkLast(E e) {
 		//暂存尾节点
        final Node<E> l = last;
        //以传入的值初始化新节点,新节点前驱为旧的尾节点l,后继为null
        final Node<E> newNode = new Node<>(l, e, null);
        //尾指针移动到新节点上
        last = newNode;
        //如果旧的尾节点是null,说明原链表是空的,因此头指针也指向新节点
        if (l == null)
            first = newNode;
        //否则旧的尾节点的后继指向新节点
        else
            l.next = newNode;
        //大小和版本号都+1
        size++;
        modCount++;
    }
addFirst(E e)

头插

public void addFirst(E e) {
	//头插入,调用linkFirst()方法
    linkFirst(e);
}

 private void linkFirst(E e) {
 		//暂存头结点
        final Node<E> f = first;
        //以传入的值初始化新节点,新节点的前驱为null,后继为原来的头结点
        final Node<E> newNode = new Node<>(null, e, f);
        //将头指针指向新节点
        first = newNode;
        //如果原来的头结点为null,说明原来的链表是空链表,那么就将尾指针也指向新节点
        if (f == null)
            last = newNode;
        //否则就让原来头结点的前驱指向新结点
        else
            f.prev = newNode;
        //大小和版本+1
        size++;
        modCount++;
    }
add(int index, E element)

按照索引位置插入

public void add(int index, E element) {
    //检查索引是否越界
    checkPositionIndex(index);
	//如果索引正好在尾部,就调用尾插的方法
    if (index == size)
        linkLast(element);
    //否则调用linkBefore()方法
    else
        linkBefore(element, node(index));
}
//检查索引是否越界
 private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
 //按照索引位置查找结点 (利用简单的二分法)  

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

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++;
    }
addAll()

addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我们平时习惯调用的addAll(Collection<? extends E>)型会转化为addAll(int, Collection<? extends E>)型,所以我们着重分析此函数即可。

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

 public boolean addAll(int index, Collection<? extends E> c) {
      //检查索引是否越界
      checkPositionIndex(index);
	//将集合转化为Object数组
        Object[] a = c.toArray();
        //如果数组长度为空(即集合为空),添加失败返回false
        int numNew = a.length;
        if (numNew == 0)
            return false;
		
        Node<E> pred, succ;
        //如果插入的位置是链表的末尾,则后继为null,前驱为原尾节点
        if (index == size) {
            succ = null;
            pred = last;
        } else {
        //否则查找原插入位置的结点,前驱为原插入位置结点的前驱
            succ = node(index);
            pred = succ.prev;
        }
		//遍历Object数组
        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;
        }
		//维护链表的长度以及版本号,添加成功返回true
        size += numNew;
        modCount++;
        return true;
    }

2.3删除方法

//按照索引删除
public E remove(int index) {
//检查索引是否越界
    checkElementIndex(index);
    //node(index)取得原索引位置的结点
    return unlink(node(index));
}
//按照值删除
 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;
    }
    
 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;
		//如果待删除结点的前驱为null,则说明待删除结点为头结点,所以头指针后移
        if (prev == null) {
            first = next;
            //否则,将待删除结点的后继指向待删除结点的后继,待删除结点的前驱断开(赋空)
        } else {
            prev.next = next;
            x.prev = null;
        }
		//如果待删除结点的后继为null,则说明待删除结点为尾节点,所以尾指针前移
        if (next == null) {
            last = prev;
            //否则将后继结点的前驱指向待删除结点的前驱,待删除结点的后继赋空
        } else {
            next.prev = prev;
            x.next = null;
        }
		//待删除结点赋空值
        x.item = null;
        //维护链表大小和版本号
        size--;
        modCount++;
       //返回删除结点
        return element;
    }   
//头删
 public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    
  //从头删除节点 f 是链表头节点
private E unlinkFirst(Node<E> f) {
    // 拿出头节点的值,作为方法的返回值
    final E element = f.item;
    // 拿出头节点的下一个节点
    final Node<E> next = f.next;
    //帮助 GC 回收头节点
    f.item = null;
    f.next = null;
    // 头节点的下一个节点成为头节点
    first = next;
    //如果 next 为空,表明链表为空
    if (next == null)
        last = null;
    //链表不为空,头节点的前一个节点指向 null
    else
        next.prev = null;
    //修改链表大小和版本
    size--;
    modCount++;
    return element;
}  
    
  //尾删  
    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }

2.4get和set方法

get

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
 //按照索引位置查找结点 (利用简单的二分法)  
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;
        }
    }
     /*体会:从源码中我们可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,这种思想值得我们借鉴。*/
     
     
      public E set(int index, E element) {
      //检查索引是否越界
        checkElementIndex(index);
        //找到索引位置的结点
        Node<E> x = node(index);
        //给结点重新复制
        E oldVal = x.item;
        x.item = element;
        //返回旧的值
        return oldVal;
    }

2.5迭代器

因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法,如下所示:

迭代顺序方法
从尾到头迭代方法hasPrevious、previous、previousIndex
从头到尾迭代方法hasNext、next、nextIndex

LinkedList 实现了 ListIterator 接口,如下图所示:

// 双向迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
    private Node<E> next;//下一个节点
    private int nextIndex;//下一个节点的位置
    //expectedModCount:期望版本号;modCount:目前最新版本号
    private int expectedModCount = modCount;
    …………
}

我们先来看下从头到尾方向的迭代:

// 判断还有没有下一个元素
public boolean hasNext() {
    return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}

// 取下一个元素
public E next() {
    //检查期望版本号有无发生变化
    checkForComodification();
    if (!hasNext())//再次检查
        throw new NoSuchElementException();
    // next 是当前节点,在上一次执行 next() 方法时被赋值的。
    // 第一次执行时,是在初始化迭代器的时候,next 被赋值的
    lastReturned = next;
    // next 是下一个节点了,为下次迭代做准备
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}

上述源码的思路就是直接取当前节点的下一个节点,而从尾到头迭代稍微复杂一点,如下:

// 如果上次节点索引位置大于 0,就还有节点可以迭代
public boolean hasPrevious() {
    return nextIndex > 0;
}
// 取前一个节点
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();
    // next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
    // next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
    lastReturned = next = (next == null) ? last : next.prev;
    // 索引位置变化
    nextIndex--;
    return lastReturned.item;
}

这里复杂点体现在需要判断 next 不为空和为空的场景,代码注释中有详细的描述。

迭代器删除

LinkedList 在删除元素时,也推荐通过迭代器进行删除,删除过程如下:

public void remove() {
    checkForComodification();
    // lastReturned 是本次迭代需要删除的值,分以下空和非空两种情况:
    // lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
    // lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
    if (lastReturned == null)
        throw new IllegalStateException();
    Node<E> lastNext = lastReturned.next;
    //删除当前节点
    unlink(lastReturned);
    // next == lastReturned 的场景分析:从尾到头递归顺序,并且是第一次迭代,并且要删除最后一个元素的情况下
    // 这种情况下,previous() 方法里面设置了 lastReturned = next = last,所以 next 和 lastReturned会相等
    if (next == lastReturned)
        // 这时候 lastReturned 是尾节点,lastNext 是 null,所以 next 也是 null,这样在 previous() 执行时,发现 next 是 null,就会把尾节点赋值给 next
        next = lastNext;
    else
        nextIndex--;
    lastReturned = null;
    expectedModCount++;
}

2.6堆和栈

总结起来如下表格:

       第一个元素(头部)                 最后一个元素(尾部)
        抛出异常        特殊值            抛出异常        特殊值
插入    addFirst(e)    offerFirst(e)    addLast(e)        offerLast(e)
移除    removeFirst()  pollFirst()      removeLast()    pollLast()
检查    getFirst()     peekFirst()      getLast()        peekLast()

(06) LinkedList可以作为FIFO(先进先出)的队列,作为FIFO的队列时,下表的方法等价:

队列方法 等效方法
add(e) — addLast(e)
offer(e) — offerLast(e)
remove() — removeFirst()
poll() ---- pollFirst()
element() — getFirst()
peek() — peekFirst()
(07) LinkedList可以作为LIFO(后进先出)的栈,作为LIFO的栈时,下表的方法等价:

栈方法 等效方法
push(e) — addFirst(e)
pop() — removeFirst()
peek() — peekFirst()

3.小结

2.2.2 Arraylist 与 LinkedList 区别?(内容来自JavaGuide,侵删)

  • 1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  • 2. 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  • 3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  • 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值