Java:ArrayList类小记

对 Java 中的 ArrayList类,做一个微不足道的小小小小记

概述

java.util.ArrayList 是大小可变的数组的实现,存储在内的数据称为元素。此类提供一些方法来操作内部存储的元素。 ArrayList 中可不断添加元素,其大小也自动增长。

  • ArrayList 是一种变长的集合类,基于定长数组实现。
  • ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。
  • 由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。
  • ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的异常或错误。

成员属性

// 序列化号
private static final long serialVersionUID = 8683452581122892189L; // 默认的容量
private static final int DEFAULT_CAPACITY = 10; // 默认的空的数组,这个主要是在构造方法初始化一个空数组的时候使用
private static final Object[] EMPTY_ELEMENTDATA = {}; // 使用默认size大小的空数组实例,和EMPTY_ELEMENTDATA区分开来,
// 这样可以知道当第一个元素添加的时候进行扩容至多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // ArrayList底层存储数据就是通过数组的形式,ArrayList长度就是数组的长度。
// 一个空的实例elementData为上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当添加第一个元素的时候会进行扩容,扩容大小就是上面的默认容量DEFAULT_CAPACITY
transient Object[] elementData; // ArrayList的大小
private int size;

构造函数

带初始容量的构造方法

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// elementData初始化为initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果传递的长度为0,就是直接使用自己已经定义的成员变量(一个空数组)
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

无参构造

public ArrayList() {
// 构造方法中将elementData初始化为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 这样可以知道当第一个元素添加的时候进行扩容至多少
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

参数为Collection类型的构造器

public ArrayList(Collection<? extends E> c) {
// 将一个参数为Collection的集合转变为ArrayList
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
// c.toArray()可能不会正确地返回一个 Object[]数组,那么使用Arrays.copyOf()方法
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
// 如果集合转换为数组之后数组长度为0,就直接使用自己的空成员变量初始化elementData
this.elementData = EMPTY_ELEMENTDATA;
}
}

ArrayList 的扩容机制

末尾添加

  1. 当使用 add 方法的时候首先调用 ensureCapacityInternal 方法,传入 size+1 进去,检查是否需要扩充 elementData 数组的大小;

    // 1.ArrayList 中的 add方法
    public boolean add(E e) {
    // 因为要添加元素,所以添加之后可能导致容量不够,所以需要在添加之前进行判断(扩容)
    ensureCapacityInternal(size + 1); // Increments modCount!!
    // 最后扩容成功/不需要扩容 才添加元素,并令size+1
    elementData[size++] = e;
    return true;
    } // 2.ArrayList 中 add 方法的 ensureCapacityInternal 方法
    private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    } // 3.calculateCapacity方法:
    // 判断传入的数组是否为空,若是,则返回DEFAULT_CAPACITY与minCapacity中的最大值
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 其中,DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 满足下面条件表示的是第一次添加元素需要的扩容操作
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 其中private static final int DEFAULT_CAPACITY = 10;
    return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
    } // 4.ensureExplicitCapacity方法:
    // 判断添加元素后的数组,是否可能发生溢出,若可能发生溢出,则进行grow扩容
    private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 涉及 fail-fast // overflow-conscious code 已经溢出了 需要扩容了
    if (minCapacity - elementData.length > 0)
    grow(minCapacity);
    }
  2. 当需要扩容,即已经发生了溢出,则调用grow函数进行扩容操作

    private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 扩容大小为原数组长度的1.5倍;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 检查新容量的大小是否小于最小需要容量,如果小于那旧将最小容量最为数组的新容量
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    // 当新容量大于MAX_ARRAY_SIZE,使用hugeCapacity比较二者
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

    其中:newCapacity = oldCapacity + (oldCapacity >> 1);扩容大小为原数组长度的1.5倍;

    newCapacity - MAX_ARRAY_SIZE当新容量大于 MAX_ARRAY_SIZE,使用hugeCapacity比较二者

    private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
    // 对minCapacity和MAX_ARRAY_SIZE进行比较
    // 若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
    // 若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
    // 其中:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
    }
  3. ArrayList 中 copy 数组的核心就是 System.arraycopy 方法,将 original 数组的所有数据复制到 copy 数组中,这是一个本地方法。

    // 在grow函数中最终调用Arrays.copyOf方法
    elementData = Arrays.copyOf(elementData, newCapacity); // 进一步:Arrays.copyOf 调用流程
    // 1.Arrays类中的copyOf方法:
    @SuppressWarnings("unchecked")
    public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
    } // 2.Arrays类中的copyOf方法:
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength]
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
    Math.min(original.length, newLength));
    return copy;
    } // 3. System类中的arraycopy方法:是一个native方法
    public static native void arraycopy(Object src, int srcPos,
    Object dest, int destPos,
    int length);

总的来说就是分两步:

  1. 扩容:把原来的数组复制到另一个内存空间更大的数组中
  2. 添加元素:把新元素添加到扩容以后的数组中

添加到指定位置

上述通过对public boolean add(E e)函数的分析,大致了解一下 ArrayList 的扩容机制,add(E e)函数为在最后添加元素,当然还有方法public void add(int index, E element)在指定的位置添加元素:

public void add(int index, E element) {
// 1.校验传递的index参数是不是合法
rangeCheckForAdd(index); // 2.还是检测是否需要扩容,上面已经分析过了
ensureCapacityInternal(size + 1); // Increments modCount!!
// 3.将 index 及其之后的所有元素都向后移一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 4.将新元素插入至 index 处
elementData[index] = element;
size++;
} // 上述的rangeCheckForAdd方法:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

移除元素

ArrayList支持两种删除元素的方式:

  • public E remove(int index):按照下标移除
  • public boolean remove(Object o):按照元素移除

按照下标删除

public E remove(int index) {
// 校验下标是否合法
rangeCheck(index);
// fail-fast机制
modCount++;
// 直接在数组中查找这个值
E oldValue = elementData(index); // 这里计算所需要移动的数目
int numMoved = size - index - 1;
// 如果这个值大于0 说明后续有元素需要左移(size=index+1)
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 移动之后,原数组中size位置null
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}

按元素删除

按照元素删除,会删除和参数匹配的第一个元素

public boolean remove(Object o) {
// 如果元素是null 遍历数组移除第一个null
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// //遍历找到第一个null元素的下标 调用下标移除元素的方法
fastRemove(index);
// 直接return了,说明的就是删除第一个找到的元素
return true;
}
} else {
// 找到元素对应的下标 调用下标移除元素的方法
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
} // 上述的按照索引删除元素的方法:
private void fastRemove(int index) {
modCount++;
// 这里计算所需要移动的数目
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

Array & ArrayList

区别:

  1. Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象,但是Array数组在存放的时候一定是同种类型的元素,而ArrayList就不一定了,因为ArrayList可以存储Object;
  2. Array大小是固定的,ArrayList的大小是动态变化的(扩容)。

什么时候更适合使用 Array:

  1. 如果列表的大小已经指定,大部分情况下是存储和遍历它们;
  2. 对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢;
  3. 如果你要使用多维数组,使用 [][]<List> 更容易。

RandomAccess 接口

可以看到 ArrayList 实现了 RandomAccess 接口

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
// ...
}

而 LinkedList 未实现该接口

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
// ...
}

说明原因:

  1. RandomAccess 接口只是一个标志接口(Marker interface),只要 List 集合实现这个接口,就能支持快速随机访问

  2. 通过查看 Collections 类中的 copy() 方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
    throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD ||
    (src instanceof RandomAccess && dest instanceof RandomAccess)) {
    // src与dest实现了RandomAccess接口,则用for循环遍历
    for (int i=0; i<srcSize; i++)
    dest.set(i, src.get(i));
    } else {
    // src 或 dest 有一没实现RandomAccess接口,则用iterator循环遍历
    ListIterator<? super T> di=dest.listIterator();
    ListIterator<? extends T> si=src.listIterator();
    for (int i=0; i<srcSize; i++) {
    di.next();
    di.set(si.next());
    }
    }
    }

    实现 RandomAccess 接口的 List 集合采用一般的 for 循环遍历,而未实现这接口则采用迭代器,即 ArrayList 一般采用 for 循环遍历,而 LinkedList 一般采用迭代器遍历;

  3. ArrayList 用 for 循环遍历比 iterator 迭代器遍历快,LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。所以说,当我们在做项目时,应该考虑到 List 集合的不同子类采用不同的遍历方式,能够提高性能。

ArrayList & LinkedList

虽然还没看过 LinkedList 的源码,这里就先贴一个 ArrayList & LinkedList 的区别

ArrayList:底层是基于动态数组实现的,查找快,增删较慢

LinkedList:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6 之前是双向循环链表、JDK1.7 之后取消了循环),查找慢、增删快。LinkedList 链表由一系列表项连接而成,一个表项包含 3 个部分:元素内容、前驱表和后驱表。链表内部有一个 header 表项,既是链表的开始也是链表的结尾。header 的后继表项是链表中的第一个元素,header 的前驱表项是链表中的最后一个元素。

补充:

ArrayList 的增删未必就是比 LinkedList 要慢:

  1. 如果增删都是在末尾来操作(每次调用的都是 remove()add()),此时 ArrayList 就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。

  2. 如果删除操作的位置是在中间。由于 LinkedList 的消耗主要是在遍历上( LinkedList 会比较查询的index与链表长度,选择从头还是从尾开始查找),ArrayList 的消耗主要是在移动和复制上(底层调用的是 arrayCopy() 方法,是 native 方法,native的方法还是很快的)。LinkedList 的遍历速度是要慢于 ArrayList 的复制移动速度的,如果数据量有百万级的时,还是 ArrayList 要快。

    // ArrayList
    public void add(int index, E element) {
    rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!!
    // 这个是一个native的方法
    System.arraycopy(elementData, index, elementData, index + 1,
    size - index);
    elementData[index] = element;
    size++;
    } // 比较需要插入的 index 与链表长度
    // 小于链表长度一般则从而开始遍历,反之从尾开始遍历
    Node<E> node(int index) {
    // assert isElementIndex(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;
    }
    }

参考:https://www.cnblogs.com/syp172654682/p/9817277.html

参考:

☆☆☆:https://juejin.cn/post/6844903904346374158#heading-2

https://www.cnblogs.com/SunArmy/p/9844022.html

https://mp.weixin.qq.com/s/q1r9Pno6ANUzZ9wMzA-JSg

https://blog.csdn.net/weixin_39148512/article/details/79234817

Java:ArrayList类小记的更多相关文章

  1. Java ArrayList类的简单介绍

    ArrayList类的说明: ArrayList类是List接口的实现类,java.util.ArrayList集合数据存储的结构是数组结构. 特点: 元素增删慢,查找快.(由于日常开发中使用最多的功 ...

  2. 语言小知识-Java ArrayList类 深度解析

    · 问题 1:ArrayList 的 size 和 capacity 怎么理解? 如果把 ArrayList 看作一个杯子的话,capacity 就是杯子的容积,也就是代表杯子能装多少东西,而 siz ...

  3. Java ArrayList类

    ArrayList对象可以用于存储一个对象列表 例子: ArrayList<String> list = new ArrayList<String>() 例子: public ...

  4. 12. java ArrayList类

    一.ArrayList定义 ​ java.util.ArrayList是大小可变的数组的实现,存储在内的数据成为元素.此类提供一些方法来操作内部存储的元素.ArrayList中可不断添加元素,其大小也 ...

  5. Java:LinkedList类小记

    Java:LinkedList类小记 对 Java 中的 LinkedList类,做一个微不足道的小小小小记 概述 java.util.LinkedList 集合数据存储的结构是循环双向链表结构.方便 ...

  6. Java API —— ArrayList类 & Vector类 & LinkList类

    1.ArrayList类     1)ArrayList类概述         · 底层数据结构是数组,查询快,增删慢         · 线程不安全,效率高     2)ArrayList案例   ...

  7. Java中ArrayList类详解

    1.什么是ArrayList ArrayList就是传说中的动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了如下一些好处: 动态的增加和减少元素 实现了ICollection和ILis ...

  8. JDK1.8源码(五)——java.util.ArrayList 类

    关于 JDK 的集合类的整体介绍可以看这张图,本篇博客我们不系统的介绍整个集合的构造,重点是介绍 ArrayList 类是如何实现的. 1.ArrayList 定义 ArrayList 是一个用数组实 ...

  9. java基础之集合框架--使用ArrayList类动态 存储数据

    一.ArrayList是List接口下的一个实现类,实现了长度可变的.连续的数组:拥有数组的特性. 遵循了LIst的规则:不唯一的.有序的. 如果没有增加泛型的话,集合中可以添加任何类型的数据. 使用 ...

随机推荐

  1. NOIP模拟13「工业题·卡常题·玄学题」

    T1:工业题 基本思路   这题有一个重要的小转化: 我们将原来的函数看作一个矩阵,\(f(i,j-1)*a\)相当于从\(j-1\)向右走一步并贡献a,\(f(i-1,j)*b\)相当于从\(i-1 ...

  2. Linux 网卡驱动sk_buff内核源码随笔

    这几天在调试有关网卡驱动的东西,有很多地方不清楚.有关网卡驱动部分主要有两个很重要的结构体:struct net_device 和struct sk_buff. 驱动大部分都是围绕这两个东西进行操作的 ...

  3. [CVE-2020-1956] Apache Kylin远程命令执行漏洞复现

    Apache Kylin是一个开源的.分布式的分析型数据仓库,提供Hadoop/Spark之上的 SQL 查询接口及多维分析(OLAP)能力以支持超大规模数据,最初由 eBay 开发并贡献至开源社区. ...

  4. jquery实现强制刷新

    $('iframe.active').attr('src', $('iframe.active').attr('src'));

  5. MapReduce 示例:减少 Hadoop MapReduce 中的侧连接

    摘要:在排序和reducer 阶段,reduce 侧连接过程会产生巨大的网络I/O 流量,在这个阶段,相同键的值被聚集在一起. 本文分享自华为云社区<MapReduce 示例:减少 Hadoop ...

  6. PHP的内置WEB服务器

    在很多时候,我们需要简单的运行一个小 demo 来验证一些代码或者轮子是否可用,是否可以运行起来,但是去配 nginx 或者 apache 都很麻烦,其实,PHP CLI 已经提供了一个简单的测试服务 ...

  7. 集合转数组:toArray()最优化方法探索

    优化背景 有些场景下(比如入参要求)需要将集合(比如List)转为数组类型,利用集合的toArray方法应该最为方便的,对于toArray()无参方法其返回的是Object[],强制转其他类型数组会C ...

  8. Windows 10 64位操作系统 下安装、配置、启动、登录、连接测试oracle 11g

    一.下载oracle安装包 1:详细下载安装版本可见官网:https://www.oracle.com/technetwork/database/enterprise-edition/download ...

  9. windows 安装zip 压缩

    * 到sourceforge上下载 zip-3.0-bin.zip https://sourceforge.net/projects/gnuwin32/files/zip/3.0/zip-3.0-bi ...

  10. JMeter主要元件

    配置元件 http cookie管理器 http信息头管理器 http请求默认值 统一管理 快速切换测试环境 http cache管理器 静态资源 监听器元件 查看结果树 分析查看某个请求的详情 请求 ...