认真研究Java集合之LinkedList的实现原理

简介: 认真研究Java集合之LinkedList的实现原理

LinkedList同时实现了List接口和Deque对口,也就是它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
//...
}    

当你需要使用栈或者队列时,可以考虑用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(只是一个接口的名字)。


关于栈或队列,现在首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)更好的性能。

LinkedList 没有实现 RandomAccess 接口其底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后(向前)查找,时间复杂度为 O(N)。这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。


【1】类与数据结构

① 类继承

如下所示其继承自AbstractSequentialList,实现了List、Deque、Cloneable以及Serializable。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
//...
}    

实现了Cloneable以及Serializable标明其是可以克隆及序列化的。实现了List、Deque标明其是可以作为链表或者队列使用。那么AbstractSequentialList是什么呢、

从实现上,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。


AbstractSequentialList 实现顺序访问是通过listIterator实现的,如下get方法。

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

LinkedList实现了父类AbstractSequentialList 的抽象方法listIterator。

public abstract ListIterator<E> listIterator(int index);

② 数据结构

LinkedList 的数据结构示意图


8a7187c39e644856981fba946ebddda1.png

核心属性如下所示,first和last是两个指针结点,分别表示第一个和最后一个结点。

// 元素个数
transient int size = 0;
//头指针
transient Node<E> first;
//尾指针
transient Node<E> last;

与ArrayList不同,这里没有数组。而是通过一个个Node关联起来。如下所示,Node中包含了next和prev,形成了双向链表。

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

【2】核心方法

① 添加元素

默认添加到链表末尾。

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

linkLast方法会对first和last进行初始化,然后把元素追加到链表后面,更新size和modCount。

void linkLast(E e) {
  //第一次其是null
    final Node<E> l = last;
    //  Node(Node<E> prev, E element, Node<E> next)
    //实例化新结点 prev-> l ,next->null
    final Node<E> newNode = new Node<>(l, e, null);
  //newNode作为last
    last = newNode;
    if (l == null)
      //newNode也作为first 
        first = newNode;
    else
        l.next = newNode;
    //元素数量+1    
    size++;
    // 结构变化计数器+1
    modCount++;
}

② 指定位置添加元素

如果索引位置等于size,那么直接追加到链表末尾。这种情况比较高效,否则就需要遍历链表找到index位置,插入数据element。

public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

checkPositionIndex同样是用来检测index是否合法的。

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

node(index)

用来检索目标位置的结点,返回结果是Node。如果index 小于size的一半,则从first开始正向检索,否则反向检索。

Node<E> node(int index) {
   // assert isElementIndex(index);
//如果index 小于size的一半,则从first开始正向检索
   if (index < (size >> 1)) {
       Node<E> x = first;
       for (int i = 0; i < index; i++)
           x = x.next;
       return x;
   } else {
   //否则从last开始反向检索
       Node<E> x = last;
       for (int i = size - 1; i > index; i--)
           x = x.prev;
       return x;
   }
}

linkBefore(E e, Node succ)是将元素e插入到结点succe前面。

// Node<E> succ是索引位置的元素,E e是将要插入的元素
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //获取当前结点的头结点
    final Node<E> pred = succ.prev;
    //将e实例化结点  Node(Node<E> prev, E element, Node<E> next)
    final Node<E> newNode = new Node<>(pred, e, succ);
    //当前结点prev指向新结点
    succ.prev = newNode;
    if (pred == null)//判断是否为第一个元素
        first = newNode;
    else
    //非第一个元素那么当前结点的前一个结点.next指向新结点,完成结点的插入
        pred.next = newNode;
    //更新size modCount    
    size++;
    modCount++;
}

fb6b0c2dee4147e596dd82120053ff10.png

③ 获取指定位置元素

其首先判断index,然后基于node(index)方法实现查找。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
//checkElementIndex用来判断index是否合法,[0,size)
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

④ 移除元素remove()

不指定的情况下,默认移除链表的第一个元素。

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

⑤ unlinkFirst

unlinkFirst也就是把头结点从链表中隔断。

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    //获取结点f的下一个结点
    final Node<E> next = f.next;
    //f.prev本身就是null,这里等于将first置为null,帮助垃圾回收
    f.item = null;
    f.next = null; // help GC
  //把头结点指向f的下一个结点
    first = next;
    if (next == null)//说明目前链表只有一个结点,那么first=last=null
        last = null;
    else
        next.prev = null;//否则把当前头结点的prev指向null
    //更新size   modCount  
    size--;
    modCount++;
    return element;
}

⑥ 移除指定位置元素

checkElementIndex不再赘述,node(index)用来检索目标位置的结点。

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

这里实际上交给了unlink(Node x)来处理。

unlink(Node)

这里核心思想是让当前结点的前后结点形成双向绑定关系,然后将当前结点的item、next 、 prev置为null帮助GC。

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
    size--;
    modCount++;
    //返回移除的结点中的元素
    return element;
}

⑦ 移除指定对象

remove(Object o)的核心思想就是从first开始遍历,找到期望的结点然后交给unlink(x)完成结点移除操作。

public boolean remove(Object o) {
// 当 o 为null时,使用 == 判断
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
    // 当 o 不为null,使用equals判断
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

【3】实现Deque接口的几个方法

① poll

获取并移除链表的头结点。如下所示,LinkedList交给了unlinkFirst来实现。

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

pollFirst等价于poll,检索并移除头结点

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

pollLast则是检索并移除尾结点

public E pollLast() {
   final Node<E> l = last;
   return (l == null) ? null : unlinkLast(l);
}

② peek

这个方法与element()方法一样,都是获取链表的头结点(并不移除哦)。

// 返回头结点中的item,也就是element
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
public E element() {
    return getFirst();
}

peekFirst等价于peek,都是获取头结点的元素。

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

peekLast获取尾结点的元素。

public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

③ pop

从该链表表示的堆栈中弹出一个元素。在LinkedList其实也就是移除并返回链表的头结点(中的element)与removeFirst()方法等价。

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

④ push

对于LinkedList来说,其是向头结点插入数据。

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

linkFirst

private void linkFirst(E e) {
   final Node<E> f = first;
   // Node(prev,item,next)
   final Node<E> newNode = new Node<>(null, e, f);
   first = newNode;
   if (f == null)//判断是否只有一个结点
       last = newNode;
   else
       f.prev = newNode;
   //更新size modCount    
   size++;
   modCount++;
}

⑤ offer

向链表尾部追加结点,等价于offerLast方法。

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

对应的同样有offerFirst和offerLast,分别等价于linkFirst和linkLast。

//其实就是 linkFirst(e);
public boolean offerFirst(E e) {
   addFirst(e);
   return true;
}
//其实就是linkLast
public boolean offerLast(E e) {
   addLast(e);
   return true;
}



目录
相关文章
|
11天前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
33 3
|
28天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
44 5
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
2月前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
48 4
|
2月前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
42 2
|
2月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
存储 安全 Java
LinkedList源码解读—Java8版本(上)
LinkedList源码解读—Java8版本(上)
172 0
LinkedList源码解读—Java8版本(上)
|
Java
LinkedList源码解读—Java8版本(下)
LinkedList源码解读—Java8版本(下)
140 0
|
存储 Java
LinkedList源码解读—Java8版本(中)
LinkedList源码解读—Java8版本(中)
130 0
|
5天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
44 17