ArrayList 可能是很多人使用得最为频繁的容器类了,ArrayList 实现了 List 接口,是一个有序容器,即存放元素的顺序与添加顺序相同,允许添加相同元素,包括 null ,底层通过数组来实现数据存储,容器内存储的元素个数不能超出数组空间。所以向容器中添加元素时如果发现数组空间不足,容器会自动对底层数组进行扩容操作

ArrayList 的类声明

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

从其实现的几个接口可以看出来,ArrayList 是支持快速访问,可克隆,可序列化的

包含的成员变量

//序列化IDprivate static final long serialVersionUID = 8683452581122892189L;//集合默认的初始大小private static final int DEFAULT_CAPACITY = 10;//如果外部为集合设置的初始化大小为 0,则 elementData 指向空数组对象 EMPTY_ELEMENTDATAprivate static final Object[] EMPTY_ELEMENTDATA = {};//如果在初始化集合时使用的是无参数的构造函数,则 elementData 指向空数组对象 DEFAULTCAPACITY_EMPTY_ELEMENTDATAprivate static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//包含实际元素的数组transient Object[] elementData;//集合大小private int size;

elementData 是一个 Object 类型的数组对象,即可用来存放任何对象类型,也是 ArrarList 中用来存放数据的容器。而 ArrayList 是一个泛型类,我们在初始化时就直接指定了数据类型,Java泛型只是编译器为我们提供的语法糖,方便我们在向 elementData 存取数据时,将之自动转换为特定的类型

包含的构造函数

    //指定集合的初始容量,以此来进行数组的初始化操作public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}//使用默认的初始化大小public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}//传入一份初始数据,以此进行初始化public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {this.elementData = EMPTY_ELEMENTDATA;}}

可以在初始化 ArrayList 的时候传入集合的初始化大小,这通常来说都是更为高效率一些的,因为如果是让集合在赋值的过程中自动扩容,则可能会需要进行多次扩容操作,而每次扩容都需要复制原有数据到新数组,这会导致运行效率降低

添加/修改 元素

在获取指定索引处的元素时,都是直接通过坐标指向该元素 (E) elementData[index],而无需从头开始遍历集合,所以说 ArrayList 的遍历效率较高

    //通过索引直接访问数组@SuppressWarnings("unchecked")E elementData(int index) {return (E) elementData[index];}//获取索引 index 处的元素值public E get(int index) {rangeCheck(index);return elementData(index);}//将索引 index 出的元素值置为 element,并返回原始数值public E set(int index, E element) {rangeCheck(index);E oldValue = elementData(index);elementData[index] = element;return oldValue;}

ArrayList 在存入数据时相对来说就不是那么理想了
如果是直接向集合尾端添加数据 add(E e),则先检查是否需要扩容,需要的话则创建一个新的符合大小的数组,并将原数组中的元素移到新数组中,再向数组尾端添加待存入的元素
如果是向集合非尾端位置添加数据 add(int index, E element),一样需要先检查是否需要扩容,然后将数组中索引 index 后的所有元素向后推移一位,然后将 element 插入到空出的位置上
由此看出来,向集合添加元素由于可能会导致数组扩容,从而导致数组元素的大量移动,所以说 ArrayList 存入数据的效率并不高

    //向集合添加数据public boolean add(E e) {//检查是否需要扩容ensureCapacityInternal(size + 1);//赋值elementData[size++] = e;return true;}//将元素 element 添加索引 index 位置public void add(int index, E element) {rangeCheckForAdd(index);//检查是否需要扩容ensureCapacityInternal(size + 1);//将索引 index 后的所有数值向后推移一位 System.arraycopy(elementData, index, elementData, index + 1,size - index);//将 element 插入到空出的位置elementData[index] = element;//集合大小加1size++;}

以上说的是存入单个元素,此外还有存入整个集合的情况

    //向集合添加数据//如果待添加的数据不为空则返回 true,否则返回 falsepublic boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;//检查是否需要扩容ensureCapacityInternal(size + numNew);//将数组 a 复制到 elementData 的尾端System.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;}//从指定索引处添加数据//如果待添加的数据不为空则返回 true,否则返回 falsepublic boolean addAll(int index, Collection<? extends E> c) {rangeCheckForAdd(index);Object[] a = c.toArray();int numNew = a.length;//检查是否需要扩容ensureCapacityInternal(size + numNew);//需要移动的数组元素数量int numMoved = size - index;//因为要添加的数据可能刚好是从数组最尾端开始添加,所以 numMoved 可能为 0//所以只在 numMoved > 0 的时候才需要对数组的元素值进行移动,以此空出位置给数组 aif (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew, numMoved);//将数组 a 包含的数据添加到 elementData 中System.arraycopy(a, 0, elementData, index, numNew);size += numNew;return numNew != 0;}

移除元素

再看下移除元素的方法
因为数组是一种内存地址连续的数据结构,所以移除某个元素同样可能导致大量元素的移动

    //移除指定索引处的元素值,并返回该值public E remove(int index) {rangeCheck(index);modCount++;//待移除的元素值E oldValue = elementData(index);//因为要移除元素导致需要移动的元素数量int numMoved = size - index - 1;//因为要移除的元素可能刚好是数组最后一位,所以 numMoved 可能为 0//所以只在 numMoved > 0 的时候才需要对数组的元素值进行移动if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index, numMoved);//不管数组是否需要对元素值进行移动,数组的最后一位都是无效数据了//此处将之置为 null 以帮助GC回收                elementData[--size] = null;return oldValue;}//移除集合中包含的第一位元素值为 o 的对象//如果包含该对象,则返回 true ,否则返回 falsepublic boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;}

扩容机制

以上多处说到了数组的扩容,这里就来看下数组的扩容机制
实际进行扩容操作的是 grow(int grow(int minCapacity)) 方法,参数 minCapacity 用于指定要求的最小空间,在扩容前,会先判断如果将当前容量提升到当前的 1.5 倍是否能达到 minCapacity 的要求 ,如果符合要求则直接将数据扩充到当前的 1.5 倍容量,否则则扩充到 minCapacity ,构建一个新的符合大小的数组后,就将原数组中的元素复制到新数组中
由此可想到,如果在初始化 ArrayList 前已知目标数据的数据量,则最好使用 ArrayList(int initialCapacity)来进行初始化,直接让底层数组扩充到目标大小,避免之后赋值过程中多次扩容

    //触发集合进行扩容操作,参数 minCapacity 表示集合扩容后的最小空间//如果在向集合进行赋值操作前已知数据量大小,则直接调用此方法让集合直接扩容到该大小有助于提高集合的运行效率//如果是让集合在赋值的过程中自动扩容,则可能会需要进行多次扩容操作,而每次扩容都需要复制原有数据到新数组,这会导致运行效率降低public void ensureCapacity(int minCapacity) {//1. 如果当前数组还未进行任何赋值操作,即 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则数组空间只由参数 minCapacity 影响//2. 如果当前数组已进行过赋值操作,即 elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则当前数组大小可能还是在使用默认容量 DEFAULT_CAPACITY//   则此时只有当 minCapacity 大于 DEFAULT_CAPACITY 时,才需要进行扩容int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}//检查当前的数组容量,minCapacity 用于指定当前需要的最小空间//如果数组容量没有达到一定标准,则需要进行扩容private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//数组空间至少需要扩容到 DEFAULT_CAPACITYminCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);}private void ensureExplicitCapacity(int minCapacity) {modCount++;//如果当前数组大小的确是比需要的最小空间 minCapacity 小,则进行扩容if (minCapacity - elementData.length > 0)grow(minCapacity);}//数组可扩充到的最大空间private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//对数组进行扩容private void grow(int minCapacity) {//扩容前的数组大小int oldCapacity = elementData.length;//oldCapacity >> 1 的含义即为:将 oldCapacity 值除以 2//即先假设扩容后的空间大小是原先的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);//如果 newCapacity 依然是达不到最小空间要求,则直接将空间扩大到由 minCapacity 指定的大小if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//如果扩容后的数组空间超出了最大容量限制,则将容量定为 Integer.MAX_VALUEif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);//构建符合容量大小的数组并复制原数组的数据elementData = Arrays.copyOf(elementData, newCapacity);}

遍历集合的方法

    //遍历集合元素@Overridepublic void forEach(Consumer<? super E> action) {Objects.requireNonNull(action);final int expectedModCount = modCount;@SuppressWarnings("unchecked")final E[] elementData = (E[]) this.elementData;final int size = this.size;//如果 modCount 值被改动,则直接停止遍历并抛出异常for (int i=0; modCount == expectedModCount && i < size; i++) {//将集合元素依次传递给 accept 方法action.accept(elementData[i]);}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}

遍历并过滤集合的方法

    //按照给定规则对集合元素进行过滤,如果元素符合过滤规则 filter 则将之移除@Overridepublic boolean removeIf(Predicate<? super E> filter) {Objects.requireNonNull(filter);//要移除的元素个数int removeCount = 0;//用于标记集合是哪个索引位置需要被移除final BitSet removeSet = new BitSet(size);final int expectedModCount = modCount;final int size = this.size;for (int i=0; modCount == expectedModCount && i < size; i++) {@SuppressWarnings("unchecked")final E element = (E) elementData[i];//依次判断集合元素是否符合过滤规则if (filter.test(element)) {//set 方法将导致索引位置 i 的元素变为 trueremoveSet.set(i);removeCount++;}}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}//只有 removeCount > 0 才说明需要移除元素final boolean anyToRemove = removeCount > 0;if (anyToRemove) {//集合移除指定元素后的大小final int newSize = size - removeCount;for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {//略过被标记为 true 的位置,直接跳到不需要移除元素的数组索引位i = removeSet.nextClearBit(i);//有效数据逐渐从尾部向头部聚集elementData[j] = elementData[i];}//移除尾部的无效数据,帮助GC回收for (int k=newSize; k < size; k++) {elementData[k] = null;}this.size = newSize;if (modCount != expectedModCount) {throw new ConcurrentModificationException();}modCount++;}return anyToRemove;}//将集合元素遍历传递给 operator,并将原始数据替换为 operator 的返回值@Override@SuppressWarnings("unchecked")public void replaceAll(UnaryOperator<E> operator) {Objects.requireNonNull(operator);final int expectedModCount = modCount;final int size = this.size;for (int i=0; modCount == expectedModCount && i < size; i++) {//依次传递数组元素给 apply 方法,并将其返回值替换原始数据elementData[i] = operator.apply((E) elementData[i]);}//不允许在排序的过程中集合被其它方法修改了数据结构(例如:移除元素)if (modCount != expectedModCount) {throw new ConcurrentModificationException();}modCount++;}

迭代器

在这里有个小细节,ArrayList 里多处使用到了 modCount 这个参数,每当集合的结构发生变化时,modCount 就会递增,当在对集合进行迭代操作时,迭代器会检查此参数值,如果检查到此参数的值发生变化,就说明在迭代的过程中集合的结构发生了变化,此时迭代的元素可能就并不是最新的了,因此会直接抛出异常

     //返回集合迭代器public Iterator<E> iterator() {return new Itr();}//一个优化版本的迭代器private class Itr implements Iterator<E> {//lastRet 指向的元素的下一个元素的索引int cursor;//最后一个返回的元素的索引//如果值为 -1,说明还未返回过元素或者改元素被移除了int lastRet = -1;//用于验证集合的数据结构在迭代的过程中是否被修改了int expectedModCount = modCount;//是否还有元素未被遍历public boolean hasNext() {return cursor != size;}//获取下一个元素@SuppressWarnings("unchecked")public E next() {checkForComodification();int i = cursor;//如果索引值超出集合的可索引范围则抛出异常if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;//如果索引值超出数组的可索引范围则抛出异常if (i >= elementData.length)throw new ConcurrentModificationException();//游标递增cursor = i + 1;return (E) elementData[lastRet = i];}//移除 lastRet 位置的元素public void remove() {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);//因为 lastRet 位置原始的元素被移除了,所以此时 lastRet 指向的元素是原先 lastRet+1 位置的元素cursor = lastRet;lastRet = -1;//因为是 Itr 主动对集合进行修改,所以此处需要主动更新 expectedModCount 值,避免之后抛出异常expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}//遍历集合从索引 cursor 开始之后剩下的元素@Override@SuppressWarnings("unchecked")public void forEachRemaining(Consumer<? super E> consumer) {Objects.requireNonNull(consumer);final int size = ArrayList.this.size;int i = cursor;if (i >= size) {return;}final Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length) {throw new ConcurrentModificationException();}//遍历调用 accept 方法while (i != size && modCount == expectedModCount) {consumer.accept((E) elementData[i++]);}cursor = i;lastRet = i - 1;checkForComodification();}//判断迭代器在遍历集合的过程中,集合是否被外部改动了(例如被其它迭代器移除了元素)//如果是的话则抛出异常final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}

效率测试

最后来测试下 ArrayList 扩容次数的高低对其运行效率的影响
对三个 ArrayList 存入相同数据量的数据,但分别为 ArrayList 指定不同的初始化大小

public static void main(String[] args) {//开始时间long startTime = System.currentTimeMillis();List<String> stringList = new ArrayList<>();for (int i = 0; i < 300000; i++) {stringList.add("leavesC " + i);}//结束时间long endTime = System.currentTimeMillis();System.out.println("不指定初始大小,所用时间:" + (endTime - startTime) + "毫秒");//开始时间startTime = System.currentTimeMillis();List<String> stringList2 = new ArrayList<>(100000);for (int i = 0; i < 300000; i++) {stringList2.add("leavesC " + i);}//结束时间endTime = System.currentTimeMillis();System.out.println("指定初始大小为目标数据量的三分之一,所用时间:" + (endTime - startTime)+ "毫秒");//开始时间startTime = System.currentTimeMillis();List<String> stringList3 = new ArrayList<>(300000);for (int i = 0; i < 300000; i++) {stringList3.add("leavesC " + i);}//结束时间endTime = System.currentTimeMillis();System.out.println("指定初始大小为目标数据量,所用时间:" + (endTime - startTime)+ "毫秒");}

可以看出来,各种方式之间的运行效率差距还是很大的

关于 ArrayList 的内容就讲到这里了,一方面是篇幅所限,一方面我是觉得很多知识点其实也不需要怎么讲,直接看源码的话认知会更为深刻一点,因此我也把对 ArrayList 的详细源码注释开源到了 GitHub 上,欢迎关注

源码地址:Java_Android_Learn

Java集合框架源码解析之ArrayList相关推荐

  1. 【java集合框架源码剖析系列】java源码剖析之ArrayList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...

  2. Java集合框架源码详解系列(一)

     写在前面:大家好!我是晴空๓.如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教.我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/.非常 ...

  3. Java集合---LinkedList源码解析

    一.源码解析 1. LinkedList类定义 2.LinkedList数据结构原理 3.私有属性 4.构造方法 5.元素添加add()及原理 6.删除数据remove() 7.数据获取get() 8 ...

  4. 【java集合框架源码剖析系列】java源码剖析之java集合中的折半插入排序算法

    注:关于排序算法,博主写过[数据结构排序算法系列]数据结构八大排序算法,基本上把所有的排序算法都详细的讲解过,而之所以单独将java集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...

  5. java集合-HashSet源码解析

    HashSet 无序集合类 实现了Set接口 内部通过HashMap实现 // HashSet public class HashSet<E>extends AbstractSet< ...

  6. java校验框架源码解析_Spring Boot原理剖析和源码分析

    Spring Boot原理剖析和源码分析 依赖管理 问题一:为什么导入dependency时不需要指定版本? spring-boot-starter-parent依赖 org.springframew ...

  7. Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap

    Java LinkedHashMap和HashMap有什么区别和联系?为什么LinkedHashMap会有着更快的迭代速度?LinkedHashSet跟LinkedHashMap有着怎样的内在联系?本 ...

  8. Java集合类框架源码分析 之 LinkedList源码解析 【4】

    上一篇介绍了ArrayList的源码分析[点击看文章],既然ArrayList都已经做了介绍,那么作为他同胞兄弟的LinkedList,当然必须也配拥有姓名! Talk is cheap,show m ...

  9. java 并发框架源码_Java并发编程高阶技术-高性能并发框架源码解析与实战

    Java并发编程高阶技术-高性能并发框架源码解析与实战 1 _0 Z' @+ l: s3 f6 r% t|____资料3 Z9 P- I2 x8 T6 ^ |____coding-275-master ...

最新文章

  1. 互联网协议 — UDP 用户数据报协议
  2. 小学计算机教师德育工作计划,小学教师个人德育工作计划5篇.docx
  3. CO CCA-作业分割优先级
  4. RocketMQ部署安装(非Docker安装)
  5. Linuxday01基础命令
  6. java url接口_javaweb 后台使用url接口
  7. Qt: 找不到Qt5Widgets.lib
  8. Adbshell相关命令
  9. 利用OpenCV抠图技术实现影视中“隐身”特效
  10. leetcode390(2022 1.2)
  11. 根据.jgwx配准文件绘制并加载图层
  12. L2-011 玩转二叉树 (25分)
  13. 数据库 的日志已满,备份该数据库的事务日志以释放一些日志空间的解决办法 ...
  14. [高效Mac] 多显示器快速切换鼠标焦点和移动窗口
  15. 生命游戏(python版)
  16. excel冻结窗口_冻结窗口怎么冻结多行
  17. 时间局部性和空间局部性
  18. [每日短篇] 5 - Sublime Text 的正则表达式 Capturing Group
  19. 热电阻PT100转4-20mA温度信号转换器,变送分配器
  20. 从虎胆龙威4(live free or die hard)说黑客攻击

热门文章

  1. 程序员面试【Brainteasers】
  2. 在PowerShell中创建对象并添加属性成员
  3. C#实现注册码功能编程总结
  4. Django项目:CRM(客户关系管理系统)--50--41PerfectCRM实现全局账号密码修改
  5. win10+anaconda+cuda配置dlib,使用GPU对dlib的深度学习算法进行加速(以人脸检测为例)...
  6. 又想到了模板引擎和前端MVVM框架
  7. 作业1--求100内的奇数。
  8. 下载服务器文件到本地
  9. C#_List转换成DataTable
  10. 微软Azure已开始支持hadoop--大数据云计算