java集合框架List子接口之LinkedList源码剖析

简介: LinkendList从物理结构来说是一种线性结构 , 而从逻辑结构来说是一种链式存储结构 , 虽然是线性结构 , 但是并不会按照线性的顺序存储 , 而是分散存储在内存中 , 通过指针来建立节点与节点之间的联系, 同时实现了Deque这也说明它是一个双向链表 , 在源码中 , 没有看到它对线程安全问题的处理 , 所以它同时还是一个线程不安全的链表 , 也没有看到不允许插入null元素 , 重复元素的处理 , 所以它又是一个允许重复元素以及null元素的链表

LinkedList

LinkendList是一个双向链表 , 并且实现了Deque接口 , 可以作为一个队列来使用 , 虽然LinkendList是线性结构 , 但是数据的存储并不是按照线性的接口来存储的 , 而是在每一个节点里存数据及下一个节点的地址, 同时实现了Cloneable接口 , 支持拷贝 , 并且实现了java.io.Serializable支持序列化和反序列化


Cloneable , Serializable接口直通车 : java框架集合List子接口之ArrayList源码剖析


数据结构直通车 : 数据结构之顺序存储结构和链式存储结构分析 , 图文并茂 , 又涨姿势了


队列直通车 : 线性数据结构之队列(Queue)


Deque

Deque是double ended queue的简称,支持两端的元素插入和移除 ,习惯上称之为双端队列。大多数Deque 实现对它们可能包含的元素的数量没有固定的限制,但是该接口支持容量限制的deques以及没有固定大小限制的deque


Deque同时扩展了Queue接口,当Deque作为队列的时候,会产生FIFO(先进先出)行为。元素添加在双端队列的末尾并从头开始删除。


双端队列的功能就是既可以从队首添加 , 也可以从队尾添加 , 既可以从队尾获取 , 也可以从队首获取


Deque和Queue方法对比


Deque

Queue

添加元素到队尾 add(E e) / offer(E e)

addLast(E e)


offerLast(E e)


取队首元素并删除

E remove()


E poll()


E removeFirst()


E pollFirst()


取队首元素但不删除

E element()


E peek()


E getFirst()


E peekFirst()


添加元素到队首 无

addFirst(E e)


offerFirst(E e)


取队尾元素并删除 无

E removeLast()


E pollLast()


取队尾元素但不删除 无

E getLast()


E peekLast()


可以看出LinkendList又是一个集合 , 又是一个队列 , 又是一个栈 , 功能还是比较强大的 , 但是我们在使用的时候,总是用特定的接口来引用它,这是因为持有接口说明代码的抽象层次更高,而且接口本身定义的方法代表了特定的用途。

// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
d1.offerLast("z");
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");

可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。


LinkendList构造方法


构造函数 描述

LinkedList() 此构造函数构建一个空链表。

LinkedList(Collection c) 此构造函数构建一个链表,它使用集合c的元素进行初始化。

  // 当前列表的节点个数
  transient int size = 0;
  // 第一个节点
  transient Node<E> first;
  // 最后一个节点 
  transient Node<E> last;
    /**
     * 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);
    }

LinkendList常用方法

方法 描述

void add(int index,Object element) 将指定元素插入此列表中的指定位置索引。如果指定的索引超出范围(index<0 或 index> size()),则抛出IndexOutOfBoundsException异常。

boolean add(Object o)` 将指定的元素追加到此列表的末尾。

boolean addAll(Collection c) 将指定集合中的所有元素按指定集合的迭代器返回的顺序附加到此列表的末尾。如果指定的集合为null,则抛出NullPointerException异常。

boolean addAll(int index, Collection c) 从指定位置开始,将指定集合中的所有元素插入此列表。如果指定的集合为null,则抛出NullPointerException异常。

void addFirst(Object o) 在此列表的开头插入给定元素。

void addLast(Object o) 将给定元素追加到此列表的末尾。

void clear() 从此列表中删除所有元素。

Object clone() 返回此LinkedList的浅表副本。

boolean contains(Object o) 如果此列表包含指定的元素,则返回true。当且仅当此列表包含至少一个元素e时才返回true,即,(o == null?e == null:o.equals(e))。

Object get(int index) 返回此列表中指定位置的元素。如果指定的索引超出范围(index < 0 或 index> = size()),则抛出IndexOutOfBoundsException异常。

Object getFirst() 返回此列表中的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。

Object getLast() 返回此列表中的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。

int indexOf(Object o) 返回指定元素第一次出现的列表中的索引,如果列表不包含此元素,则返回-1。

int lastIndexOf(Object o) 返回指定元素最后一次出现的列表中的索引,如果列表不包含此元素,则返回-1。

public ListIterator<E> listIterator(int index) 从列表中的指定位置开始,返回此列表中元素的列表迭代器(按正确顺序)。如果指定的索引超出范围(index < 0 或 index> = size()),则抛出IndexOutOfBoundsException异常。

Object remove(int index) 删除此列表中指定位置的元素。如果此列表为空,则抛出NoSuchElementException异常。

boolean remove(Object o) 删除此列表中第一次出现的指定元素。如果此列表为空,则抛出NoSuchElementException异常。如果指定的索引超出范围(index < 0 或 index >= size()),则抛出IndexOutOfBoundsException异常。

Object removeFirst() 从此列表中删除并返回第一个元素。如果此列表为空,则抛出NoSuchElementException异常。

Object removeLast() 从此列表中删除并返回最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。

Object set(int index, Object element) 用指定的元素替换此列表中指定位置的元素。如果指定的索引超出范围(index < 0 或 index >= size()),则抛出IndexOutOfBoundsException异常。

int size() 返回此列表中的元素数量。

Object[] toArray() 以正确的顺序返回包含此列表中所有元素的数组。如果指定的数组为null,则抛出NullPointerException异常。

Object[] toArray(Object[] a) 以正确的顺序返回包含此列表中所有元素的数组; 返回数组的运行时类型是指定数组的运行时类型。

addAll()

 

    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 = null;
            pred = last;
        } else {
            // 找到index所在的节点 
            succ = node(index);
            // index所在位置的前一个节点
            pred = succ.prev;
        }
        // 遍历需要添加的集合,逐个插入
        for (Object o : a) {
            // 创建一个新的节点,以 pred 为前一个节点,值为 e,null 为后一个节点
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            // 如果 pred 为空,说明是在头部插入
            if (pred == null) 
                // 也就是说新建的节点是第一个节点
                first = newNode; 
            else // pred 不为空,说明是在中间或者尾部插入
                // pred 的下一个节点连接上新创建的节点
                pred.next = newNode; 
            // 依次插入
            pred = newNode; 
        }
        // 如果 succ 为空,说明是在尾部插入
        if (succ == null) { 
            // 所以最后插入的元素就是最后一个元素
            last = pred;
        } else { // succ 不为空,说明是在中间插入
            // 最后插入的元素连接上后面的一段
            pred.next = succ;
            // 后面的一段第一个元素连接上前面的一段
            succ.prev = pred;
        }
        // 数量合并
        size += numNew;
        // 修改次数+1
        modCount++;
        return true;
    }

检查数组下标越界方法

private void checkPositionIndex(int index) {
  if (!isPositionIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
  return index >= 0 && index <= size;

看到了很熟悉的异常 IndexOutOfBoundsException,就是根据链表大小检查一下


时间复杂度

addAll()属于一种浅拷贝的方式 , 通过for循环来把一个集合的元素遍历添加到另一个几集合 , 所以时间复杂度是O(N)


add()
public boolean add(E e) {
  linkLast(e);
  return true;
}
public void add(int index, E element) {
  // 检查 index 是否越界,上面分析过了
  checkPositionIndex(index);
  // 插入位置与当前数量相同,说明是尾部插入
  if (index == size) 
    linkLast(element);
  else
    linkBefore(element, node(index));
}
public void addFirst(E e) {
  linkFirst(e);
}
public void addLast(E e) {
  linkLast(e);
}

可以看到 , 主要调用了linkBefore() linkFirst() linkLast() 这几个方法 , 所以我们接下来分析这几个方法


// 在某个节点前插入一个元素
void linkBefore(E e, Node<E> succ) {
  // assert succ != null;
  // 获取到 succ 的上一个节点
  final Node<E> pred = succ.prev; 
  // 创建一个新的节点,连接到 succ 上一个节点后面
  final Node<E> newNode = new Node<>(pred, e, succ);
  // 将 succ 连接到 newNode 后面
  succ.prev = newNode; 
  // 如果 succ 的上一个节点为空,说明 succ 为头部节点
  if (pred == null) 
    // 直接将 newNode 设为头部节点
    first = newNode; 
  else // 如果 succ 的上一个节点不为空,说明 succ 为中间或者尾部节点
    // 将 succ 的上一个节点关联到 newNode 上
    pred.next = newNode; 
  size++;
  modCount++;
}


linkFirst()和linkLast()逻辑相似 , 所以就分析linkFirst

// 插入一个元素到头节点
private void linkFirst(E e) {
  final Node<E> f = first; // 当前第一个节点
  // 创建了一个新节点,以 null 为前一个节点、e 为值、当前第一个节点为下一个节点
  final Node<E> newNode = new Node<>(null, e, f);
  first = newNode; // 设置新建的节点为第一个节点
  if (f == null) // 当前第一个节点为空,说明列表为空
    last = newNode; // 所以最后一个节点为当前插入的节点
  else // 当前第一个节点不为空,说明列表不为空
    f.prev = newNode; // 当前列表头部连接上插入的节点
  size++;
  modCount++;
}


时间复杂度

add()方法有三种增加方式 , 头插 , 尾插 , 中间插入 , 头插和尾插时间复杂度为O(1) , 因为不需要通过遍历找到上一个节点 , 但是中间插入时间复杂度为O(N) , 因为需要调用node(int index)找到下标对应的元素 , 然后在这个元素后面插入


get()
public E get(int index) {
  // 检查 index 是否越界,上面分析过了
  checkElementIndex(index);
  return node(index).item;
}
Node<E> node(int index) {
  // 如果 index 小于 size 的一半,从开头开始查找 
  if (index < (size >> 1)) {
    Node<E> x = first;
    // 从头开始查找,直到 i == index
    for (int i = 0; i < index; i++)
      x = x.next;
    return x;
  } else { // 如果 index 大于 size 的一半,从尾部开始查找
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
      x = x.prev;
    return x;
  }
}



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

时间复杂度

get()方法的时间复杂度也要分开说明 , 因为LinkendList的实现中保存了头尾元素 , 所以可以直接获取 , 时间复杂度为O(1) , 但是如果是根据下标获取 , 那么就要通过遍历来获取下标对应的元素 , 时间复杂度为O(N)


remove()


public E remove(int index) {
  // 检查 index 是否越界
  checkElementIndex(index);
  return unlink(node(index));
}
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);
}

可以看到 , remove()方法和add()方法类似 , 也是分别调用了unlink() unlinkFirst() unlinkLast() , 分别表示删除某个节点 , 删除头节点 , 删除尾结点


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;
  }
  // 当前节点元素设置为空,方便 GC
  x.item = null; 
  size--;
  // 修改次数+1
  modCount++;
  return element;
}
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--; // 数量 -1
  modCount++;
  return element;
}
private E unlinkLast(Node<E> l) {
  // assert l == last && l != null;
  final E element = l.item;
  final Node<E> prev = l.prev;
  l.item = null;
  l.prev = null; // help GC
  last = prev;
  if (prev == null)
    first = null;
  else
    prev.next = null;
  size--;
  modCount++;
  return element;
}



时间复杂度

删除的时间复杂度为O(1) , 直接找到需要删除的前驱节点及后继节点 , 然后将被删除元素的前驱节点指针指向后继节点即可 , 因为是双向链表 , 所以可以直接操作前驱节点 , 如果是单向链表的话就不行

clear()
public void clear() {
  // 遍历一遍全部设置为空
  for (Node<E> x = first; x != null; ) {
    Node<E> next = x.next;
    x.item = null;
    x.next = null;
    x.prev = null;
    x = next;
  }
  first = last = null;
  size = 0;
  modCount++;
}


时间复杂度

从代码的实现不难看出 , clear()方法的时间复杂度为O(N) , 因为是通过遍历然后将元素依次置空 , 所以时间复杂度为O(N)


更多方法可以查看 https://www.runoob.com/manual/jdk11api/java.base/java/util/LinkedList.html


小结

LinkendList从物理结构来说是一种线性结构 , 而从逻辑结构来说是一种链式存储结构 , 虽然是线性结构 , 但是并不会按照线性的顺序存储 , 而是分散存储在内存中 , 通过指针来建立节点与节点之间的联系, 同时实现了Deque这也说明它是一个双向链表 , 在源码中 , 没有看到它对线程安全问题的处理 , 所以它同时还是一个线程不安全的链表 , 也没有看到不允许插入null元素 , 重复元素的处理 , 所以它又是一个允许重复元素以及null元素的链表

相关文章
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
15天前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
24天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
52 5
|
22天前
|
Java 程序员 编译器
Java|如何正确地在遍历 List 时删除元素
从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。
18 3
|
22天前
|
Java 程序员
Java|List.subList 踩坑小记
不应该仅凭印象和猜测,就开始使用一个方法,至少花一分钟认真读完它的官方注释文档。
19 1
|
25天前
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
49 3
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
53 2
|
1月前
|
存储 Java 索引
Java LinkedList详解
`LinkedList`是Java集合框架中的一个重要类,实现了`List`、`Deque`和`Cloneable`接口。它基于双向链表,支持动态扩展,允许重复元素。虽然通过索引访问元素的时间复杂度为O(n),但在插入和删除操作上表现优异,时间复杂度为O(1)。常用操作包括创建、添加、获取、删除和查找元素,以及使用迭代器遍历。适用于频繁插入和删除的场景,如队列和栈的实现。
|
3月前
|
Java
用JAVA架建List集合为树形结构的代码方法
这段代码定义了一个表示树形结构的 `Node` 类和一个用于构建树形结构的 `TreeController`。`Node` 类包含基本属性如 `id`、`pid`、`name` 和 `type`,以及子节点列表 `children`。`TreeController` 包含初始化节点列表并将其转换为树形结构的方法。通过过滤和分组操作实现树形结构的构建。详情可见:[代码示例链接1](http://www.zidongmutanji.com/zsjx/43551.html),[代码效果参考链接2](https://www.257342.com/sitemap/post.html)。
43 5
|
2月前
|
Java API 开发者
代码小妙招:用Java轻松获取List交集数据
在Java中获取两个 `List`的交集可以通过 `retainAll`方法和Java 8引入的流操作来实现。使用 `retainAll`方法更为直接,但会修改原始 `List`的内容。而使用流则提供了不修改原始 `List`、更为灵活的处理方式。开发者可以根据具体的需求和场景,选择最适合的方法来实现。了解和掌握这些方法,能够帮助开发者在实际开发中更高效地处理集合相关的问题。
96 1