Java集合源码分析之LinkedList

分析完了ListQueue之后,终于可以看看LinkedList的实现了。LinkedList弥补了ArrayList增删较慢的问题,但在查找方面又逊色于ArrayList,所以在使用时需要根据场景灵活选择。对于这两个频繁使用的集合类,掌握它们的源码并正确使用,可以让我们的代码更高效。

LinkedList既实现了List,又实现了Deque,前者使它能够像使用ArrayList一样使用,后者又使它能够承担队列的职责。LinkedList内部结构是一个双向链表,我们在分析ArrayDeque时提到过这个概念,就是扩充单链表的指针域,增加一个指向前一个元素的指针previous

AbstractSequentialList

AbstractSequentialListLinkedList的父级,它继承自AbstractList,并且是一个抽象类,它主要为顺序表的链式实现提供一个骨架:

This class provides a skeletal implementation of the List interface to minimize the effort required to implement this interface backed by a "sequential access" data store (such as a linked list). For random access data (such as an array), AbstractList should be used in preference to this class.

意思是它的主要作用是提供一个实现List接口的骨架,来减少我们实现基于链式存储的实现类时所需的工作量。AbstractSequentialList并没有做很多特殊的事情,其中最主要的是提供一个方法的默认实现,并将以下方法抽象,以期有更符合场景的实现:

public abstract ListIterator<E> listIterator(int index);

其他一些方法的实现都利用了这个listIterator方法,我们不再一一查看了。下面我们分析LinkedList的实现

LinkedList的结构

LinkedList的继承结构如下所示:

LinkedList结构图

可以看到,LinkedList也实现了Cloneablejava.io.Serializable等方法,借鉴于ArrayList的经验,我们可以想到它的Clone也是浅克隆,在序列化方法也采用了同样的方式,我们就不再赘述了。

构造方法与成员变量

数据单元Node

在介绍链表结构时提到过,其数据单元分为数据域和指针域,分别存储数据和指向下一个元素的位置,在java中只要定义一个实体类就可以解决了。

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成员变量主要有三个,而且其意义清晰可见。

// 记录当前链表的长度
transient int size = 0;

// 第一个节点
transient Node<E> first;

// 最后一个节点
transient Node<E> last;

构造函数

因为链表没有长度方面的问题,所以也不会涉及到扩容等问题,其构造函数也十分简洁了。

public LinkedList() {
}

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

一个默认的构造函数,什么都没有做,一个是用其他集合初始化,调用了一下addAll方法。addAll方法我们就不再分析了,它应该是和添加一个元素的方法是一致的。

重要方法

LinkedList既继承了List,又继承了Deque,那它必然有一堆addremoveaddFirstaddLast等方法。这些方法的含义也相差不大,实现也是类似的,因此LinkedList又提取了新的方法,来简化这些问题。我们看看这些不对外的方法,以及它们是如何与上述函数对应的。

//将一个元素链接到首位
private void linkFirst(E e) {
    //先将原链表存起来
    final Node<E> f = first;
    //定义一个新节点,其next指向原来的first
    final Node<E> newNode = new Node<>(null, e, f);
    //将first指向新建的节点
    first = newNode;
    //原链表为空表
    if (f == null)
        //把last也指向新建的节点,现在first与last都指向了它
        last = newNode;
    else
        //把原链表挂载在新建节点,也就是现在的first之后
        f.prev = newNode;
    size++;
    modCount++;
}

//与linkFirst类似
void linkLast(E e) {
    //...
}

 //在某个非空节点之前添加元素
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //先把succ节点的前置节点存起来
    final Node<E> pred = succ.prev;
    //新节点插在pred与succ之间
    final Node<E> newNode = new Node<>(pred, e, succ);
    //succ的prev指针移到新节点
    succ.prev = newNode;
    //前置节点为空
    if (pred == null)
        //说明插入到了首位
        first = newNode;
    else
        //把前置节点的next指针也指向新建的节点
        pred.next = newNode;
    size++;
    modCount++;
}

//删除首位的元素,元素必须非空
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;
}

private E unlinkLast(Node<E> l) {
    //...
}

//删除一个指定的节点
E unlink(Node<E> x) {
    //...
}

可以看到,LinkedList提供了一系列方法用来插入和删除,但是却没有再实现一个方法来进行查询,因为对链表的查询是比较慢的,所以它是通过另外的方法来实现的,我们看一下:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

//可以说尽力了
Node<E> node(int index) {
    // assert isElementIndex(index);
    
    //size>>1就是取一半的意思
    //折半,将遍历次数减少一半
    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;
    }
}

最后,我们看下它如何对应那些继承来的方法:

//引用了node方法,需要遍历
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

//也可能需要遍历
public void add(int index, E element) {
    checkPositionIndex(index);

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

//也要遍历
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

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

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

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

public E remove() {
    return removeFirst();
}

public boolean offer(E e) {
    return add(e);
}

public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

//...

总结

LinkedList非常适合大量数据的插入与删除,但其对处于中间位置的元素,无论是增删还是改查都需要折半遍历,这在数据量大时会十分影响性能。在使用时,尽量不要涉及查询与在中间插入数据,另外如果要遍历,也最好使用foreach,也就是Iterator提供的方式。

上一篇:Java集合源码分析之Queue(三):ArrayDeque

下一篇:Java集合源码分析之Map(一):超级接口Map


我是飞机酱,如果您喜欢我的文章,可以关注我~

编程之路,道阻且长。唯,路漫漫其修远兮,吾将上下而求索。

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

推荐阅读更多精彩内容

  • 财务发钱的时候,有一部分补贴,需要贴票,我以为具体数额会通知我,结果没人理 转账到帐号一笔服务费用,我以为转账完毕...
    廿九私房菜阅读 348评论 0 0
  • 2016.12.06 微冷的一天 1 装修已经完全结束了,房子已经完全成型啦。很满意,很满足。 2虽然错过了班车,...
    娑娑杨阅读 234评论 0 0
  • “我还是很喜欢你,像每天早起都要跑的五公里,风雨不停。”这是我听过最美的告白。不知道这个兵哥对爱的坚持是否已俘获了...
    怪诞小妹阅读 535评论 3 2
  • 人之初,性本善。 不知从什么时候开始,善良被人们所遗忘! 对于善良的我们刚步入这复杂的社会,总是会有点小希望...
    X面具阅读 197评论 2 3
  • “我已经老了,有一天,在一处公共场所的大厅里,有一个男人向我走来。他主动介绍自己,他对我说:‘我认识你,永远记得你...
    Cain阅读 719评论 0 3