认真研究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;
}



目录
相关文章
|
7天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
15 2
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
11天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
11天前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
12 0
|
存储 安全 Java
LinkedList源码解读—Java8版本(上)
LinkedList源码解读—Java8版本(上)
164 0
LinkedList源码解读—Java8版本(上)
|
Java
LinkedList源码解读—Java8版本(下)
LinkedList源码解读—Java8版本(下)
134 0
|
存储 Java
LinkedList源码解读—Java8版本(中)
LinkedList源码解读—Java8版本(中)
126 0
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9