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. 软件测试2021:第一次作业——热身练习(Bug)

    案例一: 问题说明:在大学生服务外包创新创业大赛的注册页面填写密码的时候只有偶数位的密码可以通过验证,而基数位的密码不可以 原因分析:在密码验证的时候多加了一条验证,使得基数位的密码不能都通过验证 案 ...

  2. Mysql - You can't specify target table '表名' for update in FROM clause 错误解决办法

    背景 在MySQL中,写SQL语句的时候 ,可能会遇到 You can't specify target table '表名' for update in FROM clause 这样的错误 错误含义 ...

  3. openswan协商流程之(六):main_inI3_outR3()

    主模式第六包:main_inI3_outR3 1. 序言 main_inI3_outR3()函数是ISAKMP协商过程中第六包的核心处理函数的入口,第五六包主要用来验证对方的身份信息,同时此报文也是加 ...

  4. Servlet体系结构

    一.使用HttpServlet 其中,HttpServlet在重写的service()方法中对http请求的共7中提交方式进行了判断,所以只要我们只要重写对应的请求方式处理逻辑方法 doGet()和d ...

  5. Fastjson反序列化漏洞基础

    Fastjson反序列化漏洞基础 FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象. 0x0 ...

  6. jdbc核心技术-宋红康

    视频地址 JDBC核心技术 第1章:JDBC概述 1.1 数据的持久化 持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用.大多数情况下,特别是企业级应用,数据持久化意味着将 ...

  7. Windows Phone 页面之间参数传递方法

    目前对WP7开发正在研究,对页面之间参数传递进行了一个小总结,有不正确的地方,欢迎大家指正.. WP7编程采用的技术是Silverlight,页面之间参数传递的方式主要有 通过NavigationCo ...

  8. 剑指offer计划19( 搜索与回溯算法中等)---java

    1.1.题目1 剑指 Offer 64. 求1+2+-+n 1.2.解法 这题看评论区真的绝了,都是人才,各个说话都好听,我看到个还有用异常来结束的就离谱. 这题用了&&当左边为fal ...

  9. 彻底解决Hive小文件问题

    最近发现离线任务对一个增量Hive表的查询越来越慢,这引起了我的注意,我在cmd窗口手动执行count操作查询发现,速度确实很慢,才不到五千万的数据,居然需要300s,这显然是有问题的,我推测可能是有 ...

  10. flex 简单设置

    /*弹性盒子 start*/ /* ============================================================ flex:定义布局为盒模型 flex-v: ...