现在由大恶人付有杰来从增删改查几个角度轻度解析ArrayList的源码

首先ArrayList的底层数据结构非常简单,就是一个数组。

从源码第115行我们可以得出信息,他的默认数组长度是10。

/*** Default initial capacity.*/private static final int DEFAULT_CAPACITY = 10;

那么我们经常调用的size方法是什么呢?
源码第281行,142行

 /*** Returns the number of elements in this list.*返回链表中元素的个数* @return the number of elements in this list*/public int size() {return size;}
 /*** The size of the ArrayList (the number of elements it contains).*同上* @serial*/private int size;

另外,还有一个关键的属性:

//modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1protected transient int modCount = 0;

以上表达的意思就是说,ArrayList的默认大小是10,内部记录有自己被修改的次数,和链表中有效的元素。所谓有效的元素就是你自己添加的元素。

1.构造方法

有三种构造方法:

1.指定大小初始化

 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);}}

清晰明了哈,如果指定的大小是大于0的,那么就用这个数字初始化,否则就初始一个空的数组。如果是非法输入(<0),就会抛出IllegalArgumentException异常。
2.无参构造函数初始化

 public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

我们可以看到,无参构造 函数并不是一来就 初始化了10个长的数组,而是初始化了一个空的数组。这样能够省点空间吧。面试官问起来了,初始化的大小是10码?绝对不是哈,是0。
3.指定数据初始化

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 {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}}

这个代码的意思就是,凡是继承于Collection的,爷都能初始化。List接口继承自Collection的。
演示一下2种姿势

      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);ArrayList<Integer> arrayList = new ArrayList<>(list);ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(2, 4, 5, 6, 7, 8));

是不是很方便,如果你不知道这个方法,你还要手动去add 1 2 3 4 5.

2.新增和扩容实现

新增就是往数组中添加元素,主要分成两步:

  • 判断是否需要扩容,如果需要执行扩容操作;
  • 直接赋值。
public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}
 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++;}

我们常用时第一种,第二种是在指定位置添加,把原来位置 的挤到后面去。后面的所有元素都要让一步,性能消耗会很大。
在添加元素之前,总是有一个

  //确保数组大小是否足够,不够执行扩容,size 为当前数组的大小ensureCapacityInternal(size + 1);

我们仔细想一想是吧,你添加元素,size就加1,所以就判断size+1是否满足。
ensureCapacityInternal(size + 1);做了很多事情。我把相关的代码都复制过来。

private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}
   private void ensureExplicitCapacity(int minCapacity) {//记录数组被修改(添加一个元素,肯定被 修改了呀)modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}
private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;//原来老旧的容量除以2,加上老的容量。实锤了!!1.5倍速扩容int newCapacity = oldCapacity + (oldCapacity >> 1);//如果扩容了,还是不够用,那么就用你声明的值。if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//如果扩容了,大于Integer.MaxValue 就用Inter.maxValueif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://将数据从原来的数组复制到新的数组上elementData = Arrays.copyOf(elementData, newCapacity);}

上面代码,关键地方我都给出了中文解释。希望你能明白。
所以我们可以总结一下:

  • 扩容的规则并不是翻倍,是原来容量大小 + 容量大小的一半,直白来说,扩容后的大小是原来容量的 1.5 倍;
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。
if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
  • 源码在扩容的时候,有数组大小溢出意识,就是说扩容后数组的大小下界不能小于 0,上界不能大于 Integer
    的最大值,这种意识我们可以学习。
private void rangeCheckForAdd(int index) {if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);

扩容的本质:

扩容的本质就是新开了一个扩容的数组,然后把原来数组的元素批量赋值过去,最后修改内部的elementData引用。

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://复制了原有 数组元素,然后修改elementDataelementData = Arrays.copyOf(elementData, newCapacity);}

** Arrays.copyOf是调用的 System.arraycopy,后者是本地方法**

 public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);

3.删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,我们选取根据值删除方式来进行源码说明:
代码532行:

public boolean remove(Object o) {// 如果要删除的值是 null,找到第一个值是 null 的删除if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {// 如果要删除的值不为 null,找到第一个和要删除的值相等的删除for (int index = 0; index < size; index++)// 这里是根据  equals 来判断值相等的,相等后再根据索引位置进行删除if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;
}

我们需要注意的两点是:

  • 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
  • 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,需要我们关注 equals 的具体实现。(这个和 == 的区别,不懂自己去百度哈)
    然后看看里面的fastRemove(源码544行)
private void fastRemove(int index) {// 记录数组的结构要发生变动了modCount++;// numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去// 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起int numMoved = size - index - 1;if (numMoved > 0)// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMovedSystem.arraycopy(elementData, index+1, elementData, index, numMoved);//数组最后一个位置赋值 null,帮助 GCelementData[--size] = null;
}

从源码中,我们可以看出,某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动(所以说,数组的删除性能开销真的很大)

4.迭代

如果要自己实现迭代器,实现 java.util.Iterator 类就好了,ArrayList 也是这样做的(内部类的方式),我们来看下迭代器的几个总要的参数:

int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。


迭代器一般来说有三个方法:

  • hasNext 还有没有值可以迭代
  • next 如果有值可以迭代,迭代的值是多少
  • remove 删除当前迭代的值
public boolean hasNext() {return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
}
public E next() {//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常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];
}
// 版本号比较
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();
}

从源码中可以看到,next 方法就干了两件事情,第一是检验能不能继续迭代,第二是找到迭代的值,并为下一次迭代做准备(cursor+1)。

public void remove() {// 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了if (lastRet < 0)throw new IllegalStateException();//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常checkForComodification();try {ArrayList.this.remove(lastRet);cursor = lastRet;// -1 表示元素已经被删除,这里也防止重复删除lastRet = -1;// 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount// 这样下次迭代时,两者的值是一致的了expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}
}
  • 删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了

其他:

都说数组的添加元素 的时间复杂度是O(1),真的如此吗?

如果我们直接调用Add(x)的方法,且数组容量足够这个数组挂在后面,那么时间复杂度就是1,如果触发了扩容机制,那么就是O(N),在使用add(index,e)的时候,时间复杂度一般来说不是O1,因为要移动索引 后面的元素。

什么是falilFast的机制?
在遍历过程中,如果数据被修改,就会报错。

 Iterator<Integer> iterator = arrayList.iterator();new Thread(new Runnable() {@Overridepublic void run() {arrayList.remove(2);}}).start();while (iterator.hasNext()){Thread.sleep(50);System.out.println(iterator.next());}//同上for (Integer integer : arrayList) {System.out.println(integer);}


上面提到,遍历的时候会记录modCount的值,如果和自己期望的不一样,就会报错。
fail-fast解决办法
方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

数组初始化,被加入一个值后,如果我使用 addAll 方法,一下子加入 15 个值,那么最终数组的大小是多少?
分析:在加入一个元素的时候,数组被初始化成10个,然后一下子加入15个,那么就会触发扩容,在初次扩容后,大小变成了15(1.5倍速度扩容),发现 还是不够用,就会使用1+15这个值作为容量。所以答案是16.
再贴一遍源码

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;//原来老旧的容量除以2,加上老的容量。实锤了!!1.5倍速扩容int newCapacity = oldCapacity + (oldCapacity >> 1);//如果扩容了,还是不够用,那么就用你声明的值。if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//如果扩容了,大于Integer.MaxValue 就用Inter.maxValueif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://将数据从原来的数组复制到新的数组上elementData = Arrays.copyOf(elementData, newCapacity);}

现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?
因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量拷贝的工作,造成拷贝的性能低下,所以回答说新建数组时,指定新数组的大小为 5k 即可。
所以大恶人付有杰建议,平常自己心知肚明的时候,自己手动指定大小。

还有ArrayList删不干净的问题:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,2,2,2,3));for(int i = 0;i<list.size();i++){if(list.get(i).equals(2)){list.remove(i);}}System.out.println(list.toString());

代码输出:

兄弟们自己去想吧。

深入Java集合ArrayList的源码解析相关推荐

  1. Java集合之TreeMap源码解析上篇

    上期回顾 上期我从树型结构谈到了红黑树的概念以及自平衡的各种变化(指路上期←戳),本期我将会对TreeMap结合红黑树理论进行解读. 首先,我们先来回忆一下红黑树的5条基本规则. 1.结点是红色或者黑 ...

  2. Java集合系列---List源码解析(ArrayList和LinkedList的区别)

    List源码主要讲ArrayList,LinkedList,Vector三个类 1 ArrayList ArrayList是一个底层基于数组的集合, 首先来看一下它的继承关系, public clas ...

  3. Java集合部分学习+源码解析

    Java集合 对象的容器,实现了对对象常用的操作,类似数组功能. 集合中的数据都是在内存中,当程序关闭或者重启后集合中的数据就会丢失,所以说是临时存储数据的容器 集合整体框架 Collection:单 ...

  4. Java集合---Arrays类源码解析

    一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类型: ...

  5. Java集合系列---Collection源码解析及整体框架结构

    集合的整体框架结构及依赖关系 1.Collection public interface Collection<E> extends Iterable<E> {} Collec ...

  6. Java集合系列---ConcurrentHashMap源码解析

    ConcurrentHashMap是Java并发容器的一员,jdk1.8以后的基本的数据结构和HashMap相似,也是选用了数组+链表/红黑树的结构,在jdk1,.7以前则是采用了分段锁的技术.Con ...

  7. Java集合系列---TreeMap源码解析(巨好懂!!!)

    TreeMap底层是基于红黑树实现,能实现根据key值对节点进行排序,排序可分为自然排序和定制排序. 自然排序:TreeMap的所有key必须实现Comparable接口, 定制排序:创建TreeMa ...

  8. Java集合系列---LinkedHashMap源码解析

    1 首先来看一下LinkedHashMap的继承关系 public class LinkedHashMap<K,V> extends HashMap<K,V> implemen ...

  9. Java集合系列---HashMap源码解析(超详细)

    1 HashMap 1)特性: 底层数据结构是数组+链表+红黑树运行null键和null值,,非线程安全,不保证有序,插入和读取顺序不保证一致,不保证有序,在扩容时,元素的顺序会被重新打乱 实现原理: ...

最新文章

  1. mysql 拼接sql批量执行_Mysql 学习笔记之 SQL 执行过程
  2. 【Flutter】开发 Flutter 包和插件 ( Flutter 包和插件简介 | 创建 Flutter 插件 | 创建 Dart 包 )
  3. (32)第一个驱动程序
  4. 数字电路数据选择器及其应用实验报告_科普|说说大数据是什么,及其特点与应用...
  5. 湖北工程学院计算机宿舍,湖北工程学院宿舍条件,宿舍环境图片(10篇)
  6. springboot+jsp小说在线阅读系统-java原创文学网
  7. python--手柄遥控通讯
  8. 【1】国产USB转接芯片CH347-初体验
  9. 2DPCA—二维主成分分析详解及编程
  10. Java双重检查懒汉式单例模式中volatile的作用
  11. 怎么改微信号第二次_微信号怎么改2017 微信号怎么改第二次方法介绍
  12. 【数据结构】基础:二叉树
  13. KaTeX 数学符号列表
  14. 常用商务邮箱:常用商务邮箱注册
  15. 一文带你了解 PPT 里面如何使用合适的图表
  16. 获取手机的设备信息和唯一ID
  17. Oracle11g 通过DBF文件恢复数据
  18. Vuforia Engine Android开发入门
  19. MdEditor-v3中上传照片的前后端对接(图片上传至又拍云云储存)
  20. BUUCTF 被劫持的神秘礼物 writeup

热门文章

  1. 电脑wifi已连接,登录QQ客户端有网,但打开网页没有网?
  2. 微信朋友圈营销如何做好头像_昵称_签名_背景基础设置?
  3. PDF转Word免费的软件有哪些?教给你三种转换方法
  4. 2022建筑电工(建筑特殊工种)考试题目模拟考试平台操作
  5. 微软正打造全新 Edge“Phoenix”浏览器?网友:画蛇添足?
  6. 抖音服务器维护中发不了视频,抖音视频发不出去怎么回事
  7. 用Python画动态圣诞树 学会了送给你女朋友呀~
  8. Synchonized原理
  9. 移动硬盘格式化(pc和mac共用)-菜鸟级解决方案[转]
  10. 谷歌您的个人资料_如何控制其他人可以看到的有关您的Google个人资料的信息