JDK源码分析——LinkedList

LinkedList源码分析

LinkedList简介

在这里插入图片描述

  • LinkedList 是 List 接口的链表实现,实现了所有可选的列表操作,并且允许所有元素(包含null)。除了实现 List 接口外,LinkedList 类还提供了操作头尾 get、remove、insert 元素的统一命名方法。这些操作允许链表用作 堆栈、队列或双端队列
  • 特点:
    • 有序性:存入和取出的顺序是一致的
    • 元素可以重复
    • 含有带索引的方法
    • 独有特点:数据结构是链表,但可以作为栈、队列或双端队列使用。
  • LinkedList是一个双向的链表结构。

LinkedList原理分析

LinkedList的数据结构

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    // 双向链表的头节点
    transient Node<E> first;
    // 双向链表的尾节点
    transient Node<E> last;
	// 内部的节点类
    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;
        }
    }
}
  • LinkedList 是双向链表,里面封装了一个 Node 节点类。内部并没有数组的结构,双向链表肯定存在一个头节点和尾节点。Node 节点类有两个成员变量:
    • prev:当前节点的上一个节点,头节点的上一个节点是 null;
    • next:当前节点的下一个节点,尾节点的下一个节点时 null。
  • 链表数据结构的特点:查询慢,增删快
    • 链表数据结构基本构成是一个 Node 类;
    • 每个 Node 类中,有上一个节点 prev 和下一个节点 next;
    • 链表一定存在至少两个节点,first 和 last 节点;
    • 如果 LinkedList 没有数据,first 和 last 都是 null。

LinkedList默认容量&最大容量

在这里插入图片描述

  • 没有默认容量,也没有最大容量。

LinkedList扩容机制

  • 无需扩容机制,只要你的内存足够大,可以无限制扩容下去,前提是不考虑查询效率。

为什么LinkedList查询慢、增删快

在这里插入图片描述

  • 查询慢、增删快,这是 LinkedList 的数据结构的特点,链表的数据结构就有这样的特点。这个特点是相对于数组来说的。

LinkedList源码剖析

为什么增删快?
  • add() 方法:
// 向LinkedList集合添加一个元素
public boolean add(E e) {
    linkLast(e); // 链接到链表的末尾
    return true;
}
void linkLast(E e) {
	// 将 全局末尾节点赋值给 l
	final Node<E> l = last;
	// 创建一个新节点: (上一个节点, 当前插入元素, null)
    final Node<E> newNode = new Node<>(l, e, null);
    // 将当前节点作为尾节点
    last = newNode;
    // 判断 l 节点是否为null
    if (l == null)
    	// 既是尾节点,也是头节点
        first = newNode;
    else
    	// 之前的尾节点,下一个节点是尾节点
        l.next = newNode;
    size++; // 当前集合的元素数量+1
    modCount++; // 操作集合数+1,modCount属性是修改计数器 
}
//------------------------------------------------------------------
// 向链表中部添加,参数一:添加的索引位置
public void add(int index, E element) {
	// 检查索引位是否符合要求
    checkPositionIndex(index);
	// 判断当前索引位置是否等于元素个数;若返回true,则是最后一个元素
    if (index == size)
        linkLast(element);
    else
    	// 连接到指定节点的后面【链表中部插入】
        linkBefore(element, node(index));
}
// 根据索引查询链表中节点
Node<E> node(int index) {
    // 判断索引是否小于已经存储元素的 1/2
    if (index < (size >> 1)) { // 二分查找:提高查找节点效率
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
// 将当前元素添加到指定节点之前
void linkBefore(E e, Node<E> succ) {
    // 取出当前节点的前一个节点
    final Node<E> pred = succ.prev;
    // 创建当前元素的节点:(上一个节点, 当前节点, 下一个节点)
    final Node<E> newNode = new Node<>(pred, e, succ);
	// 为指定节点上一个节点重新赋值
    succ.prev = newNode;
    // 判断当前节点的上一个节点是否为null
    if (pred == null)
        first = newNode; // 当前节点作为头部节点
    else
        pred.next = newNode; // 将新插入节点作为上一个节点的下个节点
    size++; // 新增元素+1
    modCount++; // 操作次数+1
}
  • remove() 方法:
// 删除指定索引位置的元素
public E remove(int index) {
	// 检查元素索引
    checkElementIndex(index);
    // node(index)根据索引查到要删除的节点
    // unlink()删除节点
    return unlink(node(index));
}
// 根据索引查询链表中节点
Node<E> node(int index) {
    // 判断索引是否小于 已经存储元素个数的 1/2
    if (index < (size >> 1)) { // 二分法查找:提高查找节点效率
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
// 删除一个指定节点
E unlink(Node<E> x) {
    // 获取当前节点中的元素
    final E element = x.item;
    // 获取当前节点的上一个节点
    final Node<E> next = x.next;
    // 获取当前节点的下一个节点
    final Node<E> prev = x.prev;
	// 判断上一个节点是否为null
    if (prev == null) {
    	// 若为null,说明当前节点为头部节点
        first = next;
    } else {
    	// 上一个节点的下一个节点改为下个节点
        prev.next = next;
        // 将当前节点的上一个节点置空
        x.prev = null;
    }
	// 判断下一个节点是否为null
    if (next == null) {
    	// 若为null,说明当前节点为尾部节点
        last = prev;
    } else {
    	// 下一个节点的上节点改为上个节点
        next.prev = prev;
        // 当前节点的上节点置空
        x.next = null;
    }
	// 删除当前节点内的元素
    x.item = null;
    size--; // 集合中和元素个数-1
    modCount++; // 当前集合操作数+1,modCount计数器,记录当前集合操作次数
    return element; // 返回删除的元素
}
为什么查询慢?
// 根据索引查询一个元素
public E get(int index) {
	// 检查索引是否存在
    checkElementIndex(index);
    // node(index)获取索引对应节点,获取节点中的数据item
    return node(index).item;
}
// 根据索引获取对应节点对象
Node<E> node(int index) {
    // 二分法查找索引对应的元素
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++) // 前半部分查找
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) // 后半部分查找
            x = x.prev;
        return x;
    }
}

经典大厂面试题

ArrayList的JDK8之前与之后的实现区别

  • JDK6:ArrayList 是饿汉式,直接创建一个初始化容量为 10 的数组,缺点就是占用空间较大。
  • JDK7 和 JDK 8:ArrayList 是懒汉式,一开始创建一个长度为 0 的数组,当添加第一个元素时,再创建一个初始容量为 10 的数组。

List和Map区别

List 集合

  • 单列集合:一次存一个
  • 有序集合
  • 元素可以重复
  • 带索引
  • List 集合主要有两个实现类:ArrayList 和 LinkedList

Map 集合

  • 双列集合:一次存一个
  • Key 是不允许重复的,Value 可以重复
  • 一个 Key 只能对应一个值 Value
  • Map 集合三兄弟:HashMap【无序集合】、LinkedHashMap【有序集合】、TreeMap【有序集合,自带排序能力】。

Array和ArrayList有何区别?什么时候更适合用Array?

  • Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象【底层是一个对象数组】;
  • Array 指定的大小固定不变,而 ArrayList 大小是动态的,可自动扩容;
  • Array 没有 ArrayList 方法多。
  • 尽管 ArrayList 明显是更好的选择,但也有时候 Array 比较好用,例如:
    • 列表的大小已经指定,大部分情况下是存储和遍历它们;
    • 基本数据类型使用 Array 更合适。

ArrayList与LinkedList的区别?

ArrayList

  • 优点:是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连续存放),查询快。
  • 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。

LinkedList

  • 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间时,不需要等一个连续的地址。对于新增和删除操作 add 和 remove,LinkedList 适用于要头尾操作或插入指定位置的场景。
  • 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低,查询慢,增删快。

适用场景分析:

  • 当需要对数据进行随机访问时,选用 ArrayList;
  • 当需要对数据进行多次增加删除修改时,选用 LinkedList;
  • 当然,绝大多数业务场景下,使用 ArrayList 就够了。主要是注意:最好避免 ArrayList 扩容,以及非顺序的插入。

ArrayList 是如何扩容的?

  • 如果通过无参构造的话,初始数组容量为 0,当真正对数组进行添加时,才真正分配容量。每次按1.5倍(位运算)的比率通过 copyOf 的方式扩容。

ArrayList集合加入10万数据,怎么提高效率?

  • ArrayList 的默认初始容量为 10,要插入大量数据的时候需要不断扩容,而扩容时非常影响性能的。因此,现在明确了 10 万条数据,我们可以直接在初始化的时候就设置 ArrayList 的容量,这样就可以提高效率了。

ArrayList与Vector区别?

ArrayList 和 Vector 都是用数组实现的,主要有下面三个区别:

  • Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果,而 ArrayList 不是。Vector 类中的方法很多都是 synchronized 修饰的,这样就导致了 Vector 在效率上无法与 ArrayList 相比。
  • 两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式不同。
  • Vector 可以设置增长因子,而 ArrayList 不可以,ArrayList 集合没哟增长因子。

适用场景

  • Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程不同步的,是不安全的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。实际场景下,如果需要多线程访问安全的数组,使用 CopyOnWriteArrayList 。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值