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元素的链表

相关文章
|
9月前
|
人工智能 Java 开发者
阿里出手!Java 开发者狂喜!开源 AI Agent 框架 JManus 来了,初次见面就心动~
JManus是阿里开源的Java版OpenManus,基于Spring AI Alibaba框架,助力Java开发者便捷应用AI技术。支持多Agent框架、网页配置、MCP协议及PLAN-ACT模式,可集成多模型,适配阿里云百炼平台与本地ollama。提供Docker与源码部署方式,具备无限上下文处理能力,适用于复杂AI场景。当前仍在完善模型配置等功能,欢迎参与开源共建。
3220 58
阿里出手!Java 开发者狂喜!开源 AI Agent 框架 JManus 来了,初次见面就心动~
|
8月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
8月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
716 8
|
8月前
|
存储 安全 Java
《数据之美》:Java集合框架全景解析
Java集合框架是数据管理的核心工具,涵盖List、Set、Map等体系,提供丰富接口与实现类,支持高效的数据操作与算法处理。
|
8月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
223 7
|
10月前
|
存储 缓存 安全
Java集合框架(二):Set接口与哈希表原理
本文深入解析Java中Set集合的工作原理及其实现机制,涵盖HashSet、LinkedHashSet和TreeSet三大实现类。从Set接口的特性出发,对比List理解去重机制,并详解哈希表原理、hashCode与equals方法的作用。进一步剖析HashSet的底层HashMap实现、LinkedHashSet的双向链表维护顺序特性,以及TreeSet基于红黑树的排序功能。文章还包含性能对比、自定义对象去重、集合运算实战和线程安全方案,帮助读者全面掌握Set的应用与选择策略。
1175 23
|
9月前
|
SQL Java 数据库连接
区分iBatis与MyBatis:两个Java数据库框架的比较
总结起来:虽然从技术角度看,iBATIS已经停止更新但仍然可用;然而考虑到长期项目健康度及未来可能需求变化情况下MYBATISS无疑会是一个更佳选择因其具备良好生命周期管理机制同时也因为社区力量背书确保问题修复新特征添加速度快捷有效.
795 12
|
10月前
|
安全 Java 开发者
Java集合框架:详解Deque接口的栈操作方法全集
理解和掌握这些方法对于实现像浏览器后退功能这样的栈操作来说至关重要,它们能够帮助开发者编写既高效又稳定的应用程序。此外,在多线程环境中想保证线程安全,可以考虑使用ConcurrentLinkedDeque,它是Deque的线程安全版本,尽管它并未直接实现栈操作的方法,但是Deque的接口方法可以相对应地使用。
509 12
|
10月前
|
存储 缓存 安全
Java集合框架(三):Map体系与ConcurrentHashMap
本文深入解析Java中Map接口体系及其实现类,包括HashMap、ConcurrentHashMap等的工作原理与线程安全机制。内容涵盖哈希冲突解决、扩容策略、并发优化,以及不同Map实现的适用场景,助你掌握高并发编程核心技巧。
|
10月前
|
存储 安全 Java
Java集合框架(一):List接口及其实现类剖析
本文深入解析Java中List集合的实现原理,涵盖ArrayList的动态数组机制、LinkedList的链表结构、Vector与Stack的线程安全性及其不推荐使用的原因,对比了不同实现的性能与适用场景,帮助开发者根据实际需求选择合适的List实现。
1054 0