java容器源码分析--LinkedList(JDK1.8)

本篇结构:

  • 前言
  • 数据结构
  • 重要参数
  • 常用方法
  • 源码分析
  • 疑问解答
  • 分析总结

一、前言

ArrayList和LinkedList都是很常用的容器,它们适合不同的场景。

对于随机访问和设置频繁的场景,应该选用ArrayList,因为ArrayList是基于一个动态数组的容器;而对于插入和删除频繁的场景,应该选用LinkedList,ArrayList插入和删除都需要复制数组,会消耗一定的性能,LinkedList则没有这样的顾虑。

下面就来深入源码来看看它是怎么实现的。

二、数据结构

与ArrayList不同,LinkedList是基于链表实现的。

何为链表?

链表是一种基本的线性数据结构,其和数组同为线性,但是数组是内存的物理存储上呈线性,逻辑上也是线性;而链表只是在逻辑上呈线性。在链表的每一个存储单元中不仅存储有当前的元素,还有下一个存储单元的地址,这样的可以通过地址将所有的存储单元连接在一起。

链表有单向,循环和双向之分。

LinkedList使用的是一个双向链表。

链表中的每个元素都是一个Node对象,其定义在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;
    }
}

三、重要参数

transient int size = 0;

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

ArrayList有三个重要的属性:

  1. size 是双向链表中节点的个数
  2. first 是双向链表的表头,它是双向链表节点所对应的类Node的实例
  3. ast 是双向链表的最后一个元素,它是双向链表节点所对应的类Node的实例

四、常用方法

同ArrayList相比,LinkedList多了addFirst,addLast,getFirst,getLast,removeFirst,removeLast等实现了Deque接口的相关方法(LinkedList即实现了List接口也实现了Deque接口)。

Deque又继承Queue接口,这里简单说下Queue中remove/poll、 add/offer、 element/peek的区别

1.offer,add区别:

一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。
add() 方法抛出一个 new IllegalStateException("Queue full") 异常,offer 则返回 false。

2.poll,remove区别:

remove() 和 poll() 方法都是从队列中删除第一个元素。

空集合调用remove() 方法时抛出 NoSuchElementException 异常, poll() 方法则返回 null。

3.peek,element区别:

element() 和 peek() 用于在队列的头部查询元素。

与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。

对于LinkedList,其offer方法不过是对add方法的调用,同add没有区别。

五、源码分析

5.1、构造方法

LinkedList提供了两个构造方法,一个无参构造方法,一个接收 Collection 的构造方法。

public LinkedList() {
}

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

无参构造什么也没做,只是创建了 LinkedList 对象,有参构造则调用了 addAll 方法。

5.2、添加操作

前面提过,LinkedList即实现了List接口,又实现了Deque接口,所以LinkedList既可以添加将元素添加到尾部,也可以将元素添加到指定索引位置,还可以添加添加整个集合;另外既可以在头部添加,又可以在尾部添加。

有addFirst(E e)、addLast(E e)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)、offer(E e)、offerFirst(E e)、offerLast(E e)等添加元素的方法。

1.先看List接口的添加操作

add(E e)

add(E e) 调用 linkLast 将元素插入链表尾部。

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

void linkLast(E e) {
    // 指向链表尾部
    final Node<E> l = last;
    // 以尾部为前驱节点创建一个新节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 将链表尾部指向新节点
    last = newNode;
    // 如果链表为空,那么该节点既是头节点也是尾节点
    if (l == null)
        first = newNode;
    // 链表不为空,将该结点作为原链表尾部的后继节点
    else
        l.next = newNode;
    // 长度增加
    size++;
    modCount++;
}

add(int index, E element)

add(int index, E element)在指定位置添加元素。它分三步:

  1. 先检查索引是否越界
  2. 然后判断是不是最后一个索引,是就调用 linkLast 插入尾部
  3. 不是则调用 linkBefore 插入链表中间

linkBefore 方法又调用了 node(index) node(index) 方法返回 index 处的节点,然后 linkBefore 将往index插入新节点,并更新相应节点的 prev,next 属性。

public void add(int index, E element) {
    // 检查索引是否处于[0-size]之间
    checkPositionIndex(index);

    // 索引等于size,相当于插在尾部
    if (index == size)
        linkLast(element);
    // 否则插在链表中间
    else
        linkBefore(element, node(index));
}


// 返回index处的节点
Node<E> node(int index) {
    // assert isElementIndex(index);
    
    // 为提升遍历效率,如果index小于size/2,就顺序往后遍历
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    // 如果index大于size/2,就逆向往前遍历
    } 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) {
    // assert succ != null;
    // 定义一个节点元素保存succ的prev引用,也就是index-1处的节点
    final Node<E> pred = succ.prev;
    // 根据新增的元素创建新节点,原index处的节点succ变为index+1处的节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改succ的上一个节点为插入的新节点
    succ.prev = newNode;
    // 如果pred是null 表明succ第一个节点
    if (pred == null)
        // 成员属性first指向新节点
        first = newNode;
    // 节点前元素(index-1)的next属性指向新节点
    else
        pred.next = newNode;
    size++;
    modCount++;
}

addAll方法

addAll有两个重载方法,一个参数的方法表示将集合元素添加到链表尾部,而两个参数的方法指定了开始插入的位置。

// 将集合插入到链表尾部,即开始索引位置为size
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

// 将集合从指定位置开始插入
public boolean addAll(int index, Collection<? extends E> c) {
    // 1.检查索引是否越界,index >= 0 && index <= size
    checkPositionIndex(index);

    // 2.将集合转化为数组,如果是空数组,直接返回false
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    // 3.获取插入位置index处前驱节点和后继节点(要注意,该后继节点是所有元素插入完成后的后继节点)
    Node<E> pred, succ;
    // 3-1.如果从尾部插入,前驱节点为last,后继节点为null
    if (index == size) {
        succ = null;
        pred = last;
    // 3-2.否则通过node(index)得到后继节点,进而拿到前驱节点
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    // 4.遍历数组逐个插入
    for (Object o : a) {
        @SuppressWarnings("unchecked") 
        E e = (E) o;
        // 4-1.创建新节点,前驱节点前面已经获取,后继节点为null(因为还未插入)
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            // 4-2.在这里更新新插入节点的后继节点
            pred.next = newNode;
        pred = newNode;
    }

    // 如果插入位置在尾部,重置last节点
    if (succ == null) {
        last = pred;
    // 否则,将插入的链表与先前链表连接起来
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

2.接着看Deque接口的添加操作

addFirst(E e)

addFirst(E e)将元素添加到链表头部

public void addFirst(E e) {
    linkFirst(e);
}

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

addLast(E e)

addLast(E e) 将元素添加到链表尾部

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

offer(E e)

offer(E e)方法用于将数据添加到链表尾部,其内部调用了add(E e)方法,可见 LinkedList 的 add (其 add 方法应该归属于 List 接口)和 offer 方法没有区别,并不是 Queue 接口中 add 满了就抛错,offer 返回false。

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

offerFirst(E e) & offerLast(E e)

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

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

Deque接口的几个添加方法相对简单,就不多说。

5.3、检索操作

1.List接口检索操作

get(int index)

get(int index) 获取索引index处得元素

public E get(int index) {
    // index >= 0 && index < size
    checkElementIndex(index);
    // 获取 index 的 node 节点的 item
    return node(index).item;
}

indexOf(Object o)

indexOf(Object o) 返回第一个匹配的节点所在的索引

public int indexOf(Object o) {
    int index = 0;
    // 为null
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    // 不为null
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

contains(Object o)

contains(Object o)方法检查对象o是否存在于链表中,调用indexOf(o)方法判断,不等于-1代表存在。

public boolean contains(Object o) {
    return indexOf(o) != -1;
}

2.Deque接口检索操作

getFirst() & getLast()

获取头节点元素 & 尾节点元素

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

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

peek() & element()

element()方法的内部就是使用getFirst()实现的,在链表为空时,抛出NoSuchElementException,peek直接返回null。

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

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

5.4、删除操作

1.List接口删除操作

remove(int index)

remove(int index)删除 index 索引处的节点

public E remove(int index) {
    // 1.index >= 0 && index < size
    checkElementIndex(index);
    // 2.unlink
    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;
    } else {
        prev.next = next;
        x.prev = null;
    }

    // 将待删除的节点同后继节点断开,并将后继节点与前驱节点关联
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

remove(Object o)

remove(Object o)删除指定对象

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

2.Deque接口删除操作

removeFirst() & removeLast()

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

public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

poll() & remove() & pop()

remove()和pop()内部调用了removeFirst()方法,而removeFirst()在链表为空时将抛出NoSuchElementException。

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

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

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

5.5、迭代操作

iterator()

iterator()调用listIterator(),listIterator()调用其有参重载listIterator(final int index),这里看到可以从指定索引处开始迭代。

public Iterator<E> iterator() {
    return listIterator();
}

public ListIterator<E> listIterator() {
    return listIterator(0);
}

public ListIterator<E> listIterator(final int index) {
    rangeCheckForAdd(index);

    return new ListItr(index);
}

ListItr是AbstractList中的一个私有内部类,运用起来是先判断 hasPrevious() ,如果true,则 previous(), 或者 hasNext(),如果true,则 next()。hasNext()和next()定义在其父类Itr中。

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public E previous() {
        checkForComodification();
        try {
            int i = cursor - 1;
            E previous = get(i);
            lastRet = cursor = i;
            return previous;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    public void set(E e) {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.set(lastRet, e);
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();

        try {
            int i = cursor;
            AbstractList.this.add(i, e);
            lastRet = -1;
            cursor = i + 1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

六、疑问解答

ArrayList和LinkedList的区别:

  1. ArrayList是基于动态数组的数据结构,LinkedList是基于链表的数据结构。
  2. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList遍历链表移动指针。
  3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要复制移动数据。

七、分析总结

LinkedList是基于双向链表的数据结构,下面还是同ArrayList一起分析:

ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方:

  1. 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。ArrayList主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致数组动态扩容;LinkedList开销是统一的,分配一个内部Node对象加到链表尾部。
  2. 在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的,先遍历移动指针到index位置,然后更新Node的前驱节点和后继节点。
  3. LinkedList不支持高效的随机元素访问,随机访问时,需要先顺序移动指针到索引处。
  4. ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间,因为要保留前驱节点和后继节点的引用。

总体来说:

当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;

当操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

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

推荐阅读更多精彩内容