jdk源码之LinkedList

61 阅读7分钟

一、LinkedList的成员变量

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

LinkedList类中有一个静态内部类Node,集合中的元素值就是存储在Node中的item。

LinkedList的成员变量first和last维护了该链表的首节点和尾节点,然后还维护了该链表的长度size。

由此可以看出LinkedList可以双向链表,不循环。

二、LinkedList常见api

1、add(E e)方法

image.png

如图所示往一个非空链表中插入O3元素需要做的就是维护指针a,指针b,指针last指向O3

public boolean add(E e) {
    linkLast(e);
    return true;
}
// 从方法的命名来看,新增一个元素的时候,是往链表的最后一个节点插入的。
void linkLast(E e) {
    final Node<E> l = last;  // 新开辟一个变量l指向尾节点,防止当前尾节点指向丢失
    final Node<E> newNode = new Node<>(l, e, null);  //创建节点时,维护了节点的prev,即上图的指针b,元素e,next指向空
    last = newNode;  // 该对象的尾指针指向新元素
    if (l == null)
        first = newNode;   // 链表为空的时候,该对象的头指针也指向了这个新元素
    else
        l.next = newNode;   // 链表不为空的时候,维护了上图的指针a
    size++;   //链表长度加一
    modCount++;   //链表修改次数加一
}
2、add(int index, E element)方法 往指定的位置插入一个元素

image.png

学过数据结构都知道,想要让上图O1和O2插入一个元素O3,需要维护以下a,b,c,d四个指针

image.png

public void add(int index, E element) {
    checkPositionIndex(index);  // 校验元素插入的位置,不能小于0.也不能大于对象的成员变量size,不然就断链了
    if (index == size)  // 如果元素插入的位置刚好等于成员变量size,相当于就是往链表的最后一个节点插入,代码如上续
        linkLast(element);
    else
        linkBefore(element, node(index)); // 如果元素插入的位置小于成员变量size,相当于是往链表的中间位置插入的
                                          // node(index)方法就是找到当前这个位置的元素
}
​
void linkBefore(E e, Node<E> succ) {  // 这里元素e就是我们要插入的03 succ就是上图的O2
    // assert succ != null;
    final Node<E> pred = succ.prev;  // 取O2的前置节点,防丢失,有人说first不是指向了O1吗,咋能丢失勒,这里是我画的图刚好3个元素,导致first指向了O1。
    final Node<E> newNode = new Node<>(pred, e, succ);  //存入元素值e,维护了上图指针b,指针c
    succ.prev = newNode;    // 维护了上图的指针d
    if (pred == null)
        first = newNode;   // 前置节点为空的话,就直接对象的first指针指向newNode
    else
        pred.next = newNode;  // 前置节点不为空的话,维护指针a
    size++;   //链表长度加一
    modCount++;  //链表修改次数加一
}
​
​
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;
    }
}
​
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
​
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}
3、addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c)

这两个方法都是往指定位置插入一批元素,还是借用上面的图

image.png

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
​
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
​
    Node<E> pred, succ;    // 这里还是跟之前一样,找到要插入的位置的前后节点    
    if (index == size) {   // 如果是插到最后面,succ为空就行
        succ = null;
        pred = last;
    } else {               // 元素插到中间,正常去取前后节点
        succ = node(index);
        pred = succ.prev;
    }
​
    for (Object o : a) {  //开始遍历这个集合,挨个插入
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);   // 存入元素值e,维护了上述指针b
        if (pred == null)  
            first = newNode;  // 前置节点为空的话,就直接对象的first指针指向newNode
        else
            pred.next = newNode;  // 前置节点不为空的话,维护指针a
        pred = newNode;  //  pred指向newNode,这里有人会问c,d指针不管了吗,这里pred指向newNode,后面又会有新的newNode2去维护newNode2的a,b指针,即newNode的c,d指针(实在是妙哇)。
    }
    
    //  这里维护最后一个newNode的c,d指针
    if (succ == null) {  // succ为空,即上续说的往链表的最后一个元素后面插入,那就不用维护c,d指针了,last直接指向pred即可
        last = pred;
    } else {  // succ不为空
        pred.next = succ;  // 维护指针c
        succ.prev = pred;  // 维护指针d
    }
​
    size += numNew;
    modCount++;
    return true;
}
4、addFirst(E e)和addLast(E e)
public void addLast(E e) {  // 往最后面插入元素,和add(E e)方法一致,不赘诉了
    linkLast(e);
}

addFirst相当于是往链表的最前面插入一个元素,就是数据结构链表的头插法。

image.png

想要将O3插入到O1前面,得到如下链表,就需要维护a,b指针,并将first指向O3

image.png

public void addFirst(E e) {
    linkFirst(e);
}
​
private void linkFirst(E e) {
    final Node<E> f = first;   // 新定义一个变量f指向O1,防止O1的指针丢失
    final Node<E> newNode = new Node<>(null, e, f);  // 存入元素值e,维护指针a
    first = newNode;  // first指向新节点newNode
    if (f == null)
        last = newNode;   // f为空说明链表为null,first和last同时指向newNode即可
    else
        f.prev = newNode; // f不为空,维护指针b
    size++;
    modCount++;
}
5、get(int index)
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;  // 这个很简单,上面讲过根据index取节点,这里返回节点的属性item即可
}
6、set(int index, E element)
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
7、indexOf(Object o)
public int indexOf(Object o) {  //定位元素的坐标,该方法也很简单,遍历链表
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}
8、contains(Object o)
public boolean contains(Object o) {   // 判断一个链表是否包含某个元素就是调用上面的定位元素的坐标
    return indexOf(o) != -1;   
}
9、remove()和removeFirst()
public E remove() { 
    return removeFirst();  // LinkedList的remove方法居然是移除第一个元素,我也才知道,一直以为是最后一个,我丢
}

removeFirst()方法移除第一个元素,如下图所示,我们需要做的就是断开a,b指针,first指向O2,O1释放内存

image.png

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;   // 取f的元素值,用于最后返回
    final Node<E> next = f.next;  // 找到O1的下一根节点O2
    f.item = null;                // 元素值置为null,元素值所在堆空间等待垃圾回收
    f.next = null;                //  断开指针a
    first = next;                 //  first指向O2
    if (next == null)
        last = null;              // 如果next为空,即删除完这个元素,整个链表为空了,first和last都指向了null
    else
        next.prev = null;         // 断开指针b
    size--;
    modCount++;
    return element;
}
​
// 思考一下这里为什么没有将f置为null,不置为空的话,f指向的Node数据堆内存能被垃圾回收吗。
// 这里f指向的Node堆空间,目前仅被f引用,当这个方法执行完,f从栈帧弹出,那么堆那块空间就0引用了,就是垃圾,能够被回收了
10、remove(int index)

根据指定下标删除节点,如下图所示,删除O3,需要断开指针b,指针c,指针a指向O2,指针d指向O1

image.png

image.png

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;    // 前置节点为空,说明它自身就是first,自身要被删除了,那first指向next
    } else {
        prev.next = next;  //  指针a指向O2
        x.prev = null;     //  断开指针b
    }
​
    if (next == null) {    
        last = prev;       // 后置节点为空得话,自身要被删除了,那last指向prev
    } else {
        next.prev = prev;  //  指针d指向O1
        x.next = null;     //   断开指针c
    }
​
    x.item = null;         // 元素值置为null,元素值所在堆空间等待垃圾回收
    size--;
    modCount++;
    return element;
}

三、最后

LinkedList里面的一些方法核心的一些实现应该都差不多了,还有一些poll(),offer(E e)底层也都是add和remove,大家可以自行查看。本文中很多地方提到了指针这个概念,正常java里面是没有指针这个东西的,java里面叫引用,其实都一个意思吧,指针结合数据结构更好理解吧。

喜欢文章点赞,收藏,分享,么么哒! 原创不易,转载请贴出处。