认真研究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线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
2天前
|
存储 Java 索引
Java LinkedList详解
`LinkedList`是Java集合框架中的一个重要类,实现了`List`、`Deque`和`Cloneable`接口。它基于双向链表,支持动态扩展,允许重复元素。虽然通过索引访问元素的时间复杂度为O(n),但在插入和删除操作上表现优异,时间复杂度为O(1)。常用操作包括创建、添加、获取、删除和查找元素,以及使用迭代器遍历。适用于频繁插入和删除的场景,如队列和栈的实现。
|
21天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
1天前
|
存储 算法 安全
深入理解Java中的集合框架
【9月更文挑战第34天】本文将带你走进Java的集合框架,探索其背后的设计哲学和实现细节。我们将从集合的基本概念出发,逐步深入到具体的接口和类的实现,最后通过一个实际的例子来展示如何在Java程序中高效地使用集合。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和深度理解。
9 1
|
11天前
|
存储 安全 Java
Java 常用集合分类
Java 常用集合分类
13 2
|
2月前
|
存储 算法 Java
Java中的集合框架深度解析与实践
【8月更文挑战第31天】在Java编程的海洋中,集合框架扮演着不可或缺的角色。本文将带你领略Java集合框架的魅力,从理论到实践,深入浅出地探索List、Set和Map等核心接口的使用技巧。我们将通过具体代码示例,展示如何在日常开发中高效运用这些工具,让你的代码更加优雅和高效。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往Java集合世界的大门。
|
2月前
|
存储 人工智能 Java
JAVA集合
【8月更文挑战第31天】
|
存储 SQL 关系型数据库
最新精心整理Java面试题,实现原理分析
最新精心整理Java面试题,实现原理分析
|
7天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
15天前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
下一篇
无影云桌面