java集合系列(5)LinkedList

简介: 这篇文章开始介绍LinkList。他和ArrayList有一些相似,在上一篇文章讲解 ArrayList时,我们知道ArrayList是以数组实现,它的优势是查询性能高,劣势是按顺序增删性能差。如果在不确定元素数量的情况时,不建议使用ArrayList。这种情况下,我们就可以使用LinkedList了。所以这篇文章,旨在从源码的角度进行分析和理解LinkedList。

一、LinkedList认识


1、由链表认识LinkedList


我们应该都学过数据结构与算法,如果你知道链表的实现原理,那么你就可以直接看LinkedList的基本认识了,可以跳过本小节,如果你没学过,或者是不牢固,那么就先看看本小节再往下面学习吧。


我们想要认识链表有一个例子可以形象的去表示,我们都看过警察捉贼的故事,比如说警察现在要抓一个小偷,得知小偷在A处,结果警察一去发现没有,但是A处的相关人员说小偷可能在B处,结果警察又去了B处,发现又没有,被告知可能在C处,警察又去了C处,没有之后得到情报又去了D、E、F等等。最终抓获了小偷,我们把整个抓捕过程其实就可以看成一条线索链,或者说叫做链表。我们可以看到每一处地点,我们就可以看做链表的一个节点,每一个节点要包含两个信息:一个是本地的信息,一个是下一个地点的信息。


下面使用一张图来直观的表示一下:

v2-33a25cf190543899b31380ee65906442_1440w.jpg下面我们就可以对上面的这个链表进行一个分类:


  • 单链表:就是上面我们提到的,只有下一处的地址。
  • 双链表:表示不仅存储下一处的地址,还存储上一处的

v2-5fb158a676e5da87691a02b01719f0d7_1440w.jpg

  • 循环链表:就是说整个地址构成一个循环链。


  • 有序链表:以某个标准,给链表的元素排序,比如比较内容大小、比较哈希值等

在上面我们已经看到了链表的实现方式还有分类,当然链表还有一些基本的特性,需要我们去了解:


  • 在数组中,是通过索引访问元素的。但是链表不能,只能通过链一个一个的找。这个说明了链表的优势不在快速找到元素。


  • 链表的优势在于定点删除/插入元素,因为链表影响的最多就是给定元素的左右的两个链


  • 这里就有个trick, 由于改变右边链的时候,如果不先存储右边的结点,那么右边的结点的元素就找不到了,所以改变结点的指针的时候,会先暂存下一个结点。


  • 另外单链表找不到它的父亲结点(上一个结点),所以会经常用prev来暂存上一个结点。


2、认识LinkedList


我们的LinkedList就是以双向链表实现的。既然它是以链表来实现的,所以也会有链表的基本特性。又因为其是使用双向链表来实现的,所以重点还是在于双向链表的特性

  • 链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。
  • 除了实现List接口外,LinkedList还为在列表的开头及结尾get、remove和insert元素提供了统一的命名方法。这些操作可以将链接列表当作栈,队列和双端队列来使用。


3、从继承关系看LinkedList


为此我们需要先知道LinkedList在整个java集合框架体系里面处于一个什么样的位置。一张图来说明:

v2-652ab9e7581019c64d344d381546b312_1440w.jpg

从上面我们发现LinkedList的最根部就是实现了Collection接口。下面我们去掉其他影响集合,从LinkedList为出发点来看继承关系:

v2-b7cb5089de7941f02811151fa112bc54_1440w.jpg

从上面我们可以看到,LinkedList继承的类与实现的接口如下:


Collection 接口、List 接口、Cloneable 接口、Serializable 接口、Deque 接口(5个接口)

AbstractCollection 类、AbstractList 类、AbstractSequentialList 类(3个类) 。

其中Deque定义了一个线性Collection,支持在两端插入和删除元素。


二、分析LinkedList


在上面从链表到继承关系,我相信你应该对LinkedList有一个基本的了解了。但是下面的分析才是最重要的一环。其实LinkedList中的增删改查操作底层就是基于链表的增删改查的操作,我们可以类比去记忆,然后自己动手去写一个LinkedList


1、Api操作分析


(1)节点Node结构

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

从上面的node定义,我们就可以看到这是一个双向的链表。


(2)构造方法

public LinkedList() {}
//调用  addAll(c)  表示将集合c所有元素插入链表中
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

(3)增加操作

首先是插入单个节点:

//在尾部插入一个节点: add
public boolean add(E e) {
    linkLast(e);
    return true;
}
//生成新节点 并插入到链表尾部,更新last/first 节点。
void linkLast(E e) { 
    final Node<E> l = last; 
    final Node<E> newNode = new Node<>(l, e, null);//以原尾部节点为新节点的前置节点
    last = newNode;//更新尾部节点
    if (l == null)//若原链表为空链表,需要额外更新头结点
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;//修改modCount
}
//在指定下标,index处,插入一个节点
public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
//在succ节点前,插入一个新节点e
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;
    //以前置和后置节点和元素值e 构建一个新节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)//如果之前的前置节点是空,说明succ是原头结点。所以新节点是现在的头结点
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;//修改modCount
}

增加操作一定会更改modCount,这里面涉及到了fail-fast机制。可以翻看我之前的文章。

(4)删除操作

//remove目标节点
public E remove(int index) {
    checkElementIndex(index);//这里未给出,主要是检查下表是否越界
    return unlink(node(index));//从链表上删除某节点
}
//从链表上删除x节点
E unlink(Node<E> x) {
    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++;  //修改modCount
    return element; //返回取出的元素值
}

(5)更改操作

public E set(int index, E element) {
    checkElementIndex(index); //检查越界[0,size)
    Node<E> x = node(index);//取出对应的Node
    E oldVal = x.item;//保存旧值 供返回
    x.item = element;//用新值覆盖旧值
    return oldVal;//返回旧值
}

(6)查操作

public E get(int index) {
    checkElementIndex(index);//判断是否越界 [0,size)
    return node(index).item; //调用node()方法 取出 Node节点,
}

(7)其他操作

public Object[] toArray() {
        //new 一个新数组 然后遍历链表,将每个元素存在数组里,返回
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
}


2、遍历LinkedList


(1)一般的for循环(随机访问)

int size = list.size();
for (int i=0; i<size; i++) {
    list.get(i);        
}

(2)for--each循环

for (Integer integ:list)

(3)迭代器iterator

for(Iterator iter = list.iterator(); iter.hasNext();)
    iter.next();

(4)用pollFirst()来遍历LinkedList

while(list.pollFirst() != null)

(5)用pollLast()来遍历LinkedList

while(list.pollLast() != null)

(6)用removeFirst()来遍历LinkedList

try {
    while(list.removeFirst() != null)
} catch (NoSuchElementException e) {
}

(7)用removeLast()来遍历LinkedList

try {
    while(list.removeLast() != null)
} catch (NoSuchElementException e) {
}

注意:在链表结构实现的数据集合中,最好采用Iterator或者foreach的方式遍历,效率最高。


小结:


(1)底层实现:LinkedList的实现是基于双向链表的,且头结点中不存放数据


(2)构造方法:无参构造方法直接建立一个仅包含head节点的空链表;包含Collection的构造方法,先调用无参构造方法建立一个空链表,而后将Collection中的数据加入到链表的尾部后面。


(3)查找删除:源码中都划分为该元素为null和不为null两种情况来处理,LinkedList中允许元素为null。


(4)LinkedList是基于链表实现的,因此不存在容量不足的问题,所以这里没有扩容的方法。


(5)LinkedList是基于链表实现的,因此插入删除效率高,查找效率低(虽然有一个加速动作)。


(6)注意源码中还实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用。

v2-72a295933f7e24af1402bdc76862e09f_1440w.jpg


三、linkedList与ArrayList对比分析


下面将对ArrayList与LinkedList进行对比,主要从以下方面进行


相同点


1.接口实现:都实现了List接口,都是线性列表的实现


2.线程安全:都是线程不安全的,都是基于fail-fast机制


不同点


1.底层:ArrayList内部是数组实现,而LinkedList内部实现是双向链表结构


2.接口:ArrayList实现了RandomAccess可以支持随机元素访问,而LinkedList实现了Deque可以当做队列使用


3.性能:新增、删除元素时ArrayList需要使用到拷贝原数组,而LinkedList只需移动指

针,查找元素 ArrayList支持随机元素访问,而LinkedList只能一个节点的去遍历

相关文章
|
19天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
25 2
|
24天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
19天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
27天前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
24天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
24天前
|
Java 开发者
|
2月前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
60 5
|
2月前
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
64 3
|
23天前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
22 0
|
28天前
|
Java API Apache
java集合的组内平均值怎么计算
通过本文的介绍,我们了解了在Java中计算集合的组内平均值的几种方法。每种方法都有其优缺点,具体选择哪种方法应根据实际需求和场景决定。无论是使用传统的循环方法,还是利用Java 8的Stream API,亦或是使用第三方库(如Apache Commons Collections和Guava),都可以有效地计算集合的组内平均值。希望本文对您理解和实现Java中的集合平均值计算有所帮助。
28 0