[源码分析]ArrayList和LinkedList如何实现的?我看你还有机会!(一)

简介: [源码分析]ArrayList和LinkedList如何实现的?我看你还有机会!

前言

说真的,在 Java 使用最多的集合类中,List 绝对占有一席之地的,它和 Map 一样适用于很多场景,非常方便我们的日常开发,毕竟存储一个列表的需求随处可见。尽管如此,还是有很多同学没有弄明白 List 中 ArrayListLinkedList 有什么区别,这简直太遗憾了,这两者其实都是数据结构中的基础内容,这篇文章会从基础概念开始,分析两者在 Java 中的具体源码实现,寻找两者的不同之处,最后思考它们使用时的注意事项。

这篇文章会包含以下内容。

  1. 介绍线性表的概念,详细介绍线性表中数组链表的数据结构。
  2. 进行 ArrayList 的源码分析,比如存储结构、扩容机制、数据新增、数据获取等。
  3. 进行 LinkedList 的源码分析,比如它的存储结构、数据插入、数据查询、数据删除和 LinkedList 作为队列的使用方式等。
  4. 进行 ArrayList 和 LinkedList 的总结。

线性表

要研究 ArrayListLinkedList ,首先要弄明白什么是线性表,这里引用百度百科的一段文字。

线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。

你肯定看到了,线性表在数据结构中是一种最基本、最简单、最常用的数据结构。它将数据一个接一个的排成一条线(可能逻辑上),也因此线性表上的每个数据只有前后两个方向,而在数据结构中,数组、链表、栈、队列都是线性表。你可以想象一下整整齐齐排队的样子。

微信图片_20220414161754.png线性表

看到这里你可能有疑问了,有线性表,那么肯定有非线性表喽?没错。二叉树就是典型的非线性结构了。不要被这些花里胡哨的图吓到,其实这篇文章非常简单,希望同学耐心看完点个赞

微信图片_20220414161806.png非线性接口(图片来自网络)

数组

既然知道了什么是线性表,那么理解数组也就很容易了,首先数组是线性表的一种实现。数组是由相同类型元素组成的一种数据结构,数组需要分配一段连续的内存用来存储。注意关键词,相同类型连续内存,像这样。

微信图片_20220414161809.png

数组

不好意思放错图了,像这样。

微信图片_20220414161814.png数组概念

上面的图可以很直观的体现数组的存储结构,因为数组内存地址连续,元素类型固定,所有具有快速查找某个位置的元素的特性;同时也因为数组需要一段连续内存,所以长度在初始化长度已经固定,且不能更改。Java 中的 ArrayList 本质上就是一个数组的封装。

链表

链表也是一种线性表,和数组不同的是链表不需要连续的内存进行数据存储,而是在每个节点里同时存储下一个节点的指针,又要注意关键词了,每个节点都有一个指针指向下一个节点。那么这个链表应该是什么样子呢?看图。

微信图片_20220414161817.png单向链表

哦不,放错图了,是这样。

微信图片_20220414161820.png链表存储结构(图片来自网络)

上图很好的展示了链表的存储结构,图中每个节点都有一个指针指向下一个节点位置,这种我们称为单向链表;还有一种链表在每个节点上还有一个指针指向上一个节点,这种链表我们称为双向链表。图我就不画了,像下面这样。

微信图片_20220414161823.png

双向链表

可以发现链表不必连续内存存储了,因为链表是通过节点指针进行下一个或者上一个节点的,只要找到头节点,就可以以此找到后面一串的节点。不过也因此,链表在查找或者访问某个位置的节点时,需要**O(n)的时间复杂度。但是插入数据时可以达到O(1)**的复杂度,因为只需要修改节点指针指向。

ArratList

上面介绍了线性表的概念,并举出了两个线性表的实际实现例子,既数组和链表。在 Java 的集合类 ArrayList 里,实际上使用的就是数组存储结构,ArrayList 对 Array 进行了封装,并增加了方便的插入、获取、扩容等操作。因为 ArrayList 的底层是数组,所以存取非常迅速,但是增删时,因为要移动后面的元素位置,所以增删效率相对较低。那么它具体是怎么实现的呢?不妨深入源码一探究竟。

ArrayList 存储结构

查看 ArrayList 的源码可以看到它就是一个简单的数组,用来数据存储。

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access
/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

通过上面的注释了解到,ArrayList 无参构造时是会共享一个长度为 0 的数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA. 只有当第一个元素添加时才会第一次扩容,这样也防止了创建对象时更多的内存浪费。

ArrayList 扩容机制

我们都知道数组的大小一但确定是不能改变的,那么 ArrayList 明显可以不断的添加元素,它的底层又是数组,它是怎么实现的呢?从上面的 ArrayList 存储结构以及注释中了解到,ArrayList 在初始化时,是共享一个长度为 0 的数组的,当第一个元素添加进来时会进行第一次扩容,我们可以想像出 ArrayList 每当空间不够使用时就会进行一次扩容,那么扩容的机制是什么样子的呢?

依旧从源码开始,追踪 add() 方法的内部实现。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
// 开始检查当前插入位置时数组容量是否足够
private void ensureCapacityInternal(int minCapacity) {
    // ArrayList 是否未初始化,未初始化是则初始化 ArrayList ,容量给 10.
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
// 比较插入 index 是否大于当前数组长度,大于就 grow 进行扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 扩容规则是当前容量 + 当前容量右移1位。也就是1.5倍。
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 是否大于 Int 最大值,也就是容量最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 拷贝元素到扩充后的新的 ArrayList
    elementData = Arrays.copyOf(elementData, newCapacity);
}

通过源码发现扩容逻辑还是比较简单的,整理下具体的扩容流程如下:

  1. 开始检查当前插入位置时数组容量是否足够
  2. ArrayList 是否未初始化,未初始化是则初始化 ArrayList ,容量给 10.
  3. 判断当前要插入的下标是否大于容量
  1. 不大于,插入新增元素,新增流程完毕。
  1. 如果所需的容量大于当前容量,开始扩充。
  1. 扩容规则是当前容量 + 当前容量右移1位。也就是1.5倍。
    int newCapacity = oldCapacity + (oldCapacity >> 1);
  2. 如果扩充之后还是小于需要的最小容量,则把所需最小容量作为容量。
  3. 如果容量大于默认最大容量,则使用 最大值 Integer 作为容量。
  4. 拷贝老数组元素到扩充后的新数组
  1. 插入新增元素,新增流程完毕。

ArrayList  数据新增

上面分析扩容时候已经看到了新增一个元素的具体逻辑,因为底层是数组,所以直接指定下标赋值即可,非常简单。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e; // 直接赋值
    return true;
}

但是还有一种新增数据得情况,就是新增时指定了要加入的下标位置。这时逻辑有什么不同呢?

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  // Increments modCount!!
     // 指定下标开始所有元素后移一位
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    elementData[index] = element;
    size++;
}

可以发现这种新增多了关键的一行,它的作用是把从要插入的坐标开始的元素都向后移动一位,这样才能给指定下标腾出空间,才可以放入新增的元素。

比如你要在下标为 3 的位置新增数据100,那么下标为3开始的所有元素都需要后移一位。

微信图片_20220414161827.pngArrayList 随机新增数据

由此也可以看到 ArrayList 的一个缺点,随机插入新数据时效率不高

ArrayList 数据获取

数据下标获取元素值,一步到位,不必多言

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}
相关文章
|
存储 Java
每日一道面试题之谈一谈HashMap和HashSet的区别
每日一道面试题之谈一谈HashMap和HashSet的区别
|
15天前
|
存储 Java 程序员
ArrayList 和 LinkedList 谁更适合你的项目?这篇文章告诉你!
大家好!我是小米,今天来分享 ArrayList 和 LinkedList 的区别。通过对比它们的特点和适用场景,帮助你在实际开发中做出最优选择。ArrayList 适用于查询频繁的场景,而 LinkedList 则在插入和删除操作较多时更胜一筹。希望今天的分享对你有帮助!我是小米,下次再见!
32 1
|
8月前
|
存储 Java 索引
深入学习Java集合之ArrayList的实现原理
深入学习Java集合之ArrayList的实现原理
69 0
|
Java
认真研究Java集合之ArrayList的实现原理
认真研究Java集合之ArrayList的实现原理
52 0
|
存储 安全 Java
【面试题精讲】ArrayDeque 与 LinkedList 的区别
【面试题精讲】ArrayDeque 与 LinkedList 的区别
|
存储 Java 程序员
ArrayList 的底层原理
ArrayList 是 Java 中常用的动态数组实现,它的底层是基于数组实现的。当创建一个 ArrayList 对象时,实际上是创建了一个 Object 类型的数组,初始容量为 10。当添加元素时,如果数组已满,ArrayList 会自动扩容,它会创建一个新的数组,并将原数组中的元素复制到新数组中。
947 0
ArrayList 的底层原理
|
缓存 C++
【面试:基础篇07:ArrayList与LinkedList源码分析及其性能对比】
【面试:基础篇07:ArrayList与LinkedList源码分析及其性能对比】
144 1
|
存储 索引
ArrayList与LinkedList区别源码分析
1、ArrayList是基于数组,LinkedList是基于链表 2、基于数组的ArrayList对于根据索引值查找比较高效;基于链表的LinkedList对于增加、删除操作比较高效 3、剖析CRUD:
226 0
|
Java 开发者 索引
ArrayList 原理分析| 学习笔记
快速学习 ArrayList 原理分析。
|
存储 安全 Java
java集合系列(3)ArrayList(源码分析)
这篇文章开始介绍ArrayList。ArrayList基本上是我们在平时的开发当中,使用最多的一个集合类了,它是一个其容量能够动态增长的动态数组。所以这篇文章,旨在从源码的角度进行分析和理解。为了使得文章更加有条理,还是先给出这篇文章的大致脉络: 首先,ArrayList的基本介绍和源码API(只给出方法分析,重要的方法给出详细代码)。 然后,介绍遍历ArrayList的几种方式 接下来,叙述一下ArrayList与其他集合关键字的区别和优缺点 最后,进行一个总结
247 0
java集合系列(3)ArrayList(源码分析)