Java 容器之 LinkedList

一、概述

日常开发中,相较于 ArrayListLinkedList 的使用频率是相对较少的,但是使用较少绝对不代表着它不重要,ArrayList 在访问元素的时候速度很快,而 LinkedList 的优势则展现在它添加和删除元素的速度上,所以在添加、删除元素频繁而访问较少的场景中,我们可以考虑使用 LinkedList 作为容器储存元素。

本篇文章基于源码层面,对 LinkedList 的常用方法进行一些源码的简要分析,了解其内部的工作原理,并将它和 ArrayList 做一个简要的比较。

注意:本篇文章的源码基于 JDK1.8,可能会和之前的版本有所不同

二、LinkedList 的源码分析

在源码分析中笔者分为以下 5 部分内容进行展开:

  • 双向链表
  • 类的继承关系
  • 类的属性
  • 类的构造方法
  • 常用方法

1. 双向链表

LinkedList 内部维护了一个双向链表来储存数据,而双向链表中每个节点的结构示意图如下:
在这里插入图片描述
对应于 LinkedList 中的类就是 Node 了,它是 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;
    }
}

而一条完整的双向链表如下所示:
在这里插入图片描述
其中红色箭头代表的是 prev 的指向,黑色箭头代表的是 next 的指向。需要特别注意的一点是 JDK1.8 中 LinkedList 维护的不再是一个双向循环链表而只是一个普通的双向链表

2. 类的继承关系

LinkedList 在源码中的定义如下:


public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable
 
  • 继承自 AbstractSequentialList 抽象类,这是一个双向循环结构链表类。
  • 实现了 List 接口,规定了 List 相关的操作规范。
  • 实现了 Deque 接口,能将 LinkedList 当作双端队列使用。
  • 实现了 Cloneable 接口,可拷贝。
  • 实现了 Serializable 接口,可序列化。

3. 类的属性

了解了 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 final long serialVersionUID = 876323262645176354L;
    ......
}

LinkedList 维护的成员非常少,它们的含义如下:

  • sizeLinkedList 元素的大小。
  • first:指向 LinkedList 的第一个元素,默认为空。
  • last:指向 LinkedList 的最后一个元素,默认为空。

注意到 sizefirstlast 都添加了 transient 关键字,也就是说它们不参与序列化的过程。而 firstlast 我们可以当成一个游标,分别指向双向链表的头部和尾部,如下图所示:
在这里插入图片描述

4. 类的构造方法

LinkedList 的构造方法有 2 个,如下所示:

LinkedList() 的实现如下:

public LinkedList() {
}

无参构造方法不做任何事,相当于创建了一个空链表。

有参的构造方法 LinkedList(Collection<? extends E> c) 的实现如下:

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

调用了 addAll 方法,它的实现如下:

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

调用的是它的重载方法,实现如下:

public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);  // 检测index是否合法

    Object[] a = c.toArray();
    int numNew = a.length;  // 需要添加的元素数量
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    // 如果index为size,说明是从尾部开始添加元素的
    // 此时pred指向last,succ指向null
    if (index == size) {
        succ = null;
        pred = last;
    } else {
    	// 如果是从非尾部插入的话,succ指向索引值所在节点
    	// pred指向索引值所在节点的前一个节点
        succ = node(index);
        pred = succ.prev;
    }
	// 通过遍历的方式添加元素
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        // newNode前节点指向pred,后节点为空
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
        	// pred为空时说明newNode为第一个节点,将first指向该节点
            first = newNode;
        else
        	// 不为空时将pred的后节点指向newNode
            pred.next = newNode;
        pred = newNode; // pred重新指向newNode,以便进行下一次的元素添加
    }
	// 判断succ是否为null,为null说明pred为尾节点
	// 此种情况发生在从尾部添加元素的时候,即index=size
    if (succ == null) {
    	// 如果pred为尾节点将last指向pred
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

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

其中,检测 index 是否合法的方法 checkPositionIndex 的实现如下:

private void checkPositionIndex(int index) {
    if (!this.isPositionIndex(index)) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(index));
    }
}

private boolean isPositionIndex(int index) {
    return index>= 0 && index<= this.size;
}

addAll 方法添加元素的示意图如下,分两种情况讨论:

  1. 向空链表中添加元素:
    在这里插入图片描述
  2. 向非空链表的中间添加元素:
    在这里插入图片描述
    在了解了 2 个构造方法的原理后,接下来看看 LinkedList 的常用方法。

5. 类的常用方法

LinkedList 的常用方法和 ArrayList 非常类似,有如下几个常用方法:

  • add:往 LinkedList 中添加元素,它有两个重载方法。
  • remove:从 LinkedList 中删除元素,它同样也有两个重载方法。
  • get:获取 LinkedList 中的元素。
  • indexOf:获取 LinkedList 中指定元素的索引值。
  • set:更新指定位置的元素值。
  • contains:判断 LinkedList 中是否包含参数中的元素。

5.1 add

add(E e) 的源码如下:

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

void linkLast(E e) {
    final Node<E> l = last;
    // 新元素prev指向l,next指向null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;  // 将游标last重新定位
    if (l == null)
    	// 如果l为空说明该节点为首节点,将游标first定位到newBode
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

它的示意图和前面的 addAll 方法非常类似,如下所示:
在这里插入图片描述
add(int index, E element) 方法的实现如下:

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

    if (index == size)
    	// 相等时直接调用linkLast在尾部添加元素,效果和上面add(e)一样 
        linkLast(element);
    else
        linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 找出插入位置元素的前节点
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    // 如果pred为空说明插入位置即为头节点
    if (pred == null)
        first = newNode;
    else
    	// 不为空说明pred为普通节点,将它的next指向newNode
        pred.next = newNode;
    size++;
    modCount++;
}

示意图如下所示:
在这里插入图片描述
除了上述 3 个 add 方法之外,还有 addAlladdFirstaddLast 等方法进行元素的添加,这些方法的实现大同小异,这里就留给大家自己研究。

5.2 remove

remove(Object o) 的实现如下:

public boolean remove(Object o) {
    if (o == null) {
    	// 如果o为空,从头节点开始遍历,寻找空节点进行删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
    	// 如果o不为空,从头节点开始遍历,寻找与0相匹配的节点进行删除
        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) {
    	// 如果前结点为空说明要删除的结点为首结点,直接
		// 将first游标指向next
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
    	// 如果后结点为空说明要删除的结点为尾结点,直接
		// 将last游标指向prev
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

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

示意图如下所示:
在这里插入图片描述
remove(int index) 的实现如下:

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

可以看到 remove(int) 方法也是调用 unlink 方法进行元素的删除,删除的过程和上述过程一致,这里不再赘述。

remove() 的实现如下:

public E remove() {
	// 移除首元素
    return removeFirst();
}

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;
    找出首元素的下一个元素
    final Node<E> next = f.next;
    // 将f的成员设置为null方便GC
    f.item = null;
    f.next = null; 
    // 将next设置为首元素
    first = next;
    if (next == null)
    	// 如果next为空说明该链表为空,last直接指向null
        last = null;
    else
    	// 否则将next的前结点指为null,因为此时next为首结点
        next.prev = null;
    size--;
    modCount++;
    return element;
}

除了上述 3 个 remove 方法之外,还有 removeAllremoveFirstremoveLast 等方法进行元素的删除,这些方法的实现同样都是大同小异,就留给大家自行研究吧。

5.3 get & getFirst & getLast

这 3 个方法的实现如下所示:

public E get(int index) {
	// 检查索引值是否合法
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
	// 如果index小于size/2,从首结点往后遍历找到index所在结点
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
    	// 否则的话就从尾结点往前遍历找到index所在结点
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

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

5.4 indexOf

indexOf(Object o) 的实现如下所示:

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
    	// 如果o为null就从首结点开始遍历寻找空元素所在的结点
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
   		// 如果o不为null就从首结点开始遍历寻找与0相匹配的元素所在的结点
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    // 链表中不存在o返回-1
    return -1;
}

5.5 set

set(int index, E element) 的实现如下:

public E set(int index, E element) {
	// 检查索引值是否合法
    checkElementIndex(index);
    // 通过node方法获取index对应的结点
    // node方法的分析情况get方法部分对应的介绍
    Node<E> x = node(index);
    E oldVal = x.item;
    // 更新结点的元素
    x.item = element;
    return oldVal;
}

5.6 contains & isEmpty

public boolean contains(Object o) {
	// 调用indexOf方法查看是否存在o
    return this.indexOf(o) != -1;
}

public boolean isEmpty() {
    return this.size() == 0;
}

public int size() {
    return this.size;
}

三、遍历 LinkedList

LinkedList 在遍历元素的时候有两种方式:

  • 使用 for 循环进行遍历。
for (int i = 0; i < list.size(); i++){
    result = list.get(i);
}
  • 使用 foreach 进行遍历。
for(Integer number : list){
    result = number;
}

因为 LinkedList 内部实现了 iterator 的集合类,所以使用 foreach 编译器会默认使用 iterator 进行遍历,下面做一个简单的测试:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        long startTime;
        int result;
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < 100000; i++){
            list.add(i);
        }

        startTime = System.currentTimeMillis();
        for(Integer number : list){
            result = number;
        }
        System.out.println("foreach cost " + (System.currentTimeMillis() - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++){
            result = list.get(i);
        }
        System.out.println("for loop cost " + (System.currentTimeMillis() - startTime) + " ms");
    }
}

输出结果为:

foreach cost 3 ms
for loop cost 5003 ms

可以看到,在使用 get 方法遍历链表的时候,效率非常的低下。而同样为 100,000 个数,在用 ArrayListget 方法进行遍历的时候,花费时间如下:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        long startTime;
        int result;
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++){
            list.add(i);
        }

        startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++){
            result = list.get(i);
        }
        System.out.println("for loop cost " + (System.currentTimeMillis() - startTime) + " ms");
    }
}

输出结果:

for loop cost 1 ms

可以看到 ArrayList 只花费了 1ms 就完成了对整个数组的遍历,所以我们可以归纳出以下结论:

  • LinkedList 使用 foreach 进行遍历比 for 循环进行遍历快很多,所以更加推荐使用 foreach 对 LinkedList 进行遍历。
  • 相比于 LinkedListArrayList 在遍历上的效率要更加高。

四、与 ArrayList 的比较

虽然在日常开发中 ArrayList 的使用场景比较多,但绝对没有孰优孰劣之分,ArrayListLinkedList 有各自不同的使用场景。

ArrayList 的优点在于读取元素的速度快,因为其内部维护的是一个数组,但是它的缺点也同样明显,那就是在添加、删除元素的时候需要进行数组内元素的移动,当数组过大的时候这会造成很大的开销。

LinkedList 的优点则在于添加、删除元素的速度快,因为其内部维护的是一个双向链表,添加和删除的操作对链表来说开销很小,而它的缺点则在于读取元素的速度比较慢,特别是在使用 for 循环进行数据读取的时候,速度非常的慢,LinkedList 更加推荐使用 foreach 进行遍历。

所以在添加、删除数据较少而读取数据较多的场景下,更加推荐使用 ArrayList 进行数据的存储,而相反的在添加、删除操作频繁而读取偏少的时候,更加推荐使用 LinkedList 进行数据的存储。

参考

本篇文章属于笔者的学习笔记,参考自以下博客:

https://www.cnblogs.com/leesf456/p/5308843.html
https://www.jianshu.com/p/d5ec2ff72b33

如果对于本篇博客有疑惑的可以在下方评论区给我留言,希望这篇博客对您有所帮助~

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/88562393
今日推荐