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

相关文章
|
8天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
8天前
|
消息中间件 Java 数据库连接
Java 反射最全详解 ,框架设计必掌握!
本文详细解析Java反射机制,包括反射的概念、用途、实现原理及应用场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 反射最全详解 ,框架设计必掌握!
|
5天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
30 4
|
12天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
10天前
|
Java
Java基础(13)抽象类、接口
本文介绍了Java面向对象编程中的抽象类和接口两个核心概念。抽象类不能被实例化,通常用于定义子类的通用方法和属性;接口则是完全抽象的类,允许声明一组方法但不实现它们。文章通过代码示例详细解析了抽象类和接口的定义及实现,并讨论了它们的区别和使用场景。
|
14天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。
|
10天前
|
Java 测试技术 API
Java零基础-接口详解
【10月更文挑战第19天】Java零基础教学篇,手把手实践教学!
16 1
|
10天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
9 1
|
16天前
|
Java 数据库连接 开发者
Spring 框架:Java 开发者的春天
【10月更文挑战第27天】Spring 框架由 Rod Johnson 在 2002 年创建,旨在解决 Java 企业级开发中的复杂性问题。它通过控制反转(IOC)和面向切面的编程(AOP)等核心机制,提供了轻量级的容器和丰富的功能,支持 Web 开发、数据访问等领域,显著提高了开发效率和应用的可维护性。Spring 拥有强大的社区支持和丰富的生态系统,是 Java 开发不可或缺的工具。
|
15天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3

热门文章

最新文章