LinkedList源码解析

1 序

api文档给出的解释还是可以仔细读,从中可以得到综述信息。

  • 双端队列
  • 实现了list接口的所有操作
  • 允许添加null
  • JDK版本为1.8

与ArrayList一样,linkedList本身也不是线程安全的。api解释到了,如果在多线程环境下使用list并且没有添加必要的同步代码,那么更推荐使用Collections.synchronizedList

LinkedList对象获取的遍历器满足fail-fast策略,多线程环境下的使用就要注意了。但是请不要依赖于fail-fast策略来在多线程环境下使用这个容器。这并不能保证避免一些少见、晦涩的异常。到时候就很难排查了。

基于链表的数据结构,add和remove操作linkedList的表现比ArrayList好,其add(E)与remove()操作的时间复杂度是O(1),get(int)与remove()为O(n)。ArrayList基于动态数组的数据结构与更适合随机访问的场景。

2 代码

2.1 类的继承结构

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

除了期望中的list、cloneable和序列化之外,还有一个deque队列接口和AbstractSequentialList。

实现了队列接口意味着linkedList实现了队列数据结构的一系列方法,比如pop/peek/push等等。

另一个AbstractSequentialList是一个之前从没有去注意过的另一个抽象类。一个list接口的最小化实现。其通过迭代器实现了与随机访问不同的get/set/add等一系列方法。这一系列方法可以被linkedList复用到。

API中的介绍说明LinkedList的一部分特质是和ArrayList类似的(这些特质也是List接口本身决定的)

  • 允许插入null
  • 允许重复元素

2.2 链表基础操作

和在以前数据结构的教材上看到的类似,类中包含了一个头结点和一个尾节点

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

/* LinkedList的内部类,类结构本身比较简单,一个前驱节点一个后继节点和一个用来容纳节点内容的泛型对象*/
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;
}
}

链表的插入操作和删除操作都是修改对应前驱和后继节点的指向。这个Node对象是LinkedList的一个内部类,包含一个前驱引用和一个后继引用,这样的数据结构可以帮助实现双端队列

TailInterpolation.jpg

2.2.1 构造函数

总计有两个构造函数,一个无参构造和一个接纳已有集合中元素的构造函数。

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

无参构造没有做任何多余的操作。另一个构造函数则会执行addAll方法。这个方法是可以直接调用的,向链表的指定位置插入集合列表。

public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    // 集合对象转换为Object对象数组
    Object[] a = c.toArray();
    int numNew = a.length;
    // 判断数组长度,验证有效性
    if (numNew == 0)
        return false;
    // 用于保存原有链表指定位置前后的两个Node节点
    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
    // for循环遍历对象数组,注释中说明了toArray方法生成的数组排列顺序取决于collection对象遍历器的实现逻辑
    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;
    }
    // 更新链表size字段,该字段表明链表长度
    size += numNew;
    // modCount字段累加
    modCount++;
    return true;
}

2.2.2 增删改查

boolean add(E e)

向链表添加元素,是在其表尾新增一个节点(尾插法)。如果加入的元素是链表中的第一个元素,那么首节点和尾节点会同时指向这个节点元素。

public boolean add(E e) {
    linkLast(e);
    return true;
}

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    // 新的元素,赋值给当前链表的尾节点
    last = newNode;
    // 如果之前的尾节点为空即当前add方法加入的是这个链表中唯一的一个元素,那么头结点的指向也会指向新加入的节点newNode
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 更新链表的长度size字段
    size++;
    // 累加modCount统计字段
    modCount++;
}

示例为向链表尾加入一个新元素的方法linkLast

注意Node节点是LinkedList里的一个内部类。结构简洁,包含一个prev,一个next一个泛型化的元素容器类成员item

同时add操作也对modCount计数做了累加操作。

public boolean addAll(Collection<? extends E> c)

这个方法的内部调用非常简洁,直接复用前面已经分析过的方法==addAll(int index, Collection<? extends E> c)==。位置参数直接指定为当前链表的表尾。注意这两个方法都是由public修饰,所以都可以直接使用。

boolean remove(Object o)

删除指定元素,那么需要先遍历链表检查是否确实包含了这个目标元素。然后修改该位置元素的指向,包括检查对应的prev和next指向。

// 注意区分删除的是节点内容为null的节点,不是删除null节点。这个方法的删除遍历顺序是从头节点开始遍历,找到并删除第一个匹配的元素。
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;
    if (prev == null) {
        // 如果前驱节点为null即当前要删除的节点就是链表的头结点,则将被删除节点的后继节点赋给头结点
        first = next;
    } else {
        // 如果前驱节点不为null则将要删除节点的后继指向删除节点的前驱节点的后继。然后释放被删除节点的前驱引用
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        // 如果后继节点为null即当前要删除的节点就是链表的尾结点,则将被删除节点的后继节点赋给尾结点
        last = prev;
    } else {
        // 如果后继节点不为null则将要删除节点的前驱指向删除节点的前驱节点的前驱。然后释放被删除节点的后继引用。
        next.prev = prev;
        x.next = null;
    }
    // 将被删除元素的内容item释放
    x.item = null;
    // 更新链表长度的size字段
    size--;
    // modCount统计字段累加
    modCount++;
    // 返回这个被删除的节点的内含元素内容
    return element;
}

要想在链表中访问到指定位置的元素,需要从头节点开始遍历直到指定下标的位置。这里就和随机访问存在区别和性能差异了。如果链表size很大,那么访问一个元素的操作就会耗时较多。

链表的删除操作都是将指定位置的元素的头结点指向这个元素的next节点。注意为了方便GC回收释放空间,修改了引用指向后,被删除元素的prev与next指向都一并置空,包括置空元素的item指向。

E remove(int index)

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

除了删除指定元素,还有一个删除指定位置的元素方法。这个方法需要首先检查是否有数组越界的潜在危险。找到指定位置上的元素然后执行unlink方法来解除已经存在的引用关系。

/**
 * Returns the (non-null) Node at the specified element 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;
    }
}

这个方法在增删查的操作中会被经常用到。获取指定下标位置的非空节点对象。

E get(int index)

/**
 * Returns the element at the specified position in this list.
 *
 * @param index index of the element to return
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

和上一个方法一样,获取指定位置的元素内容,首先检查数组下标是否是在正确的范围内,然后用node(index)遍历获取到目标位置的元素item内容。注意区分linkedList与ArrayList的获取指定下标元素方法的实现,这里可以区别随机访问与顺序访问。

E set(int index, E element)

public E set(int index, E element) {
    // 检查下标参数合法性
    checkElementIndex(index);
    // 获取index下标的Node节点
    Node<E> x = node(index);
    // 获取节点原有元素
    E oldVal = x.item;
    // 将新的元素值赋给这个节点
    x.item = element;
    // 返回被替换的节点的值 oldValue
    return oldVal;
}

设置指定位置的元素。

跟指定元素位置有关的操作都需要先检查入参下标是否是可用的。下标检查不通过的都会抛出数组越界异常IndexOutOfBoundsException。然后node(index)方法获取指定下标位置的元素,修改该元素的item引用并返回被替换掉的原有的item值。set方法操作成功会返回原有的旧的值。

void add(int index, E element)

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

向指定位置插入元素

  • 检查入参下标
  • 如果下标是当前list的尾巴上,直接addLast,与add方法的实现一致(尾插法)
  • 如果是在list中嵌入一个元素。那个要对应修改前后节点元素的prev与next的指向。

顺序表(如ArrayList)插入一个元素后,就要将其后的元素一一进行后移。但是链表只需要修改共计三个节点的前后指向关系。所以这里的操作成本比较,链表是更好的。所以这里就可以看到链表的插入和删除操作的优越性。

2.3 队列系列操作

因为Node的实现里面包含了前节点的引用后后续节点的引用,再加上实现了Deque接口,因此LinkedList是一个双端队列的实现。其中的一系列操作方法(在队列首、尾的插入与删除操作)都是基于前面我们提到的增删查改的操作完成的。

peek()

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

返回队首元素,不过这个方法不会删除原有队首元素。

element()

public E element() {
    return getFirst();
}

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

返回队首元素,与前一个方法不同的地方在于,如果当前list列表是一个空列表的话,会抛出异常

与上面这一组方法类似的还有一组方法,这里放在一起比较。

E poll()

E remove()

这两个方法都是删除队首元素,poll在遇到队列为空的情况下返回null而remove会抛出异常。

boolean offer(E e)

向队尾加入元素

2.4 双端队列系列操作

包含一系列的双端队列队首队尾插入、获取双端队列队首队尾元素、队首队尾删除等等。这一些列方法都是LinkedList类实现的Deque接口所要求的方法。

2.5 值得注意的地方

LinkedList实现了cloneable接口,但是其实现的克隆操作是一个浅拷贝注意ArrayList也仅仅是实现了一个浅拷贝方法

boolean removeFirstOccurrence(Object o)

删除list列表中遇到的第一个符合要求的object元素的对象。其实这个方法就是很直接的包装了remove(Object)方法。

boolean removeLastOccurrence(Object o)

删除list列表中遇到的最后一个符合要求的object元素的对象。这个方法是前一个方法的另一端。反过来从队列的尾端开始寻找。

3 总结

LinkedList的使用,主要可以与ArrayList作对比,二者有各自的适用场景和特点。用一句很简洁的话说:ArrayList适合快速随机访问操作而其插入删除的操作效率不如LinkedList。当然这几句话几乎是背下来的,至于为什么是这个结论,需要结合类的数据结构和思想来总结。

可以简要对比一下几种主要操作二者的时间复杂度:

  • add(E e) ArrayList是O(n),LinkedList是O(1)
  • remove(int n) ArrayList是O(n),LinkedList是O(1)
  • get(int n) ArrayList是O(1),LinkedList是O(n)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • 不达成共识就不离开 想想新方案,让大家都满意,没意愿,没授权,求独赢
    高刚高刚阅读 130评论 0 0
  • 《董小姐》歌词里唱到,“我爱上一匹野马,可我家里没有草原。”一度让多少失意男女感到描写的就是自己,恨自己当年没有实...
    叫我黄某某阅读 2,195评论 9 19
  • 文 / 优游 图 / 网络精选 夏天本是个花果飘香、热情奔放的日子,可偶尔被蚊子所虐,驱不走打不死,痒不堪言,总会...
    优游球阅读 210评论 0 1
  • 这个梦出现在我大三大四的时候。 我站立在一个坐落于半岛上的宾馆的二楼露台上。天气阴沉沉的,看起来随时都有可能下雨。...
    大苍阅读 169评论 0 0