前言

上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看。

ArrayList源码分析

上一节内容(传送门《https://www.cnblogs.com/CreateMyself/p/11440876.html》)我们在控制台实例化如下一个ArrayList,并添加一条数据,如下

  ArrayList<Integer> list = new ArrayList<>();
list.add(1);

初始化容量分析

首先实例化了ArrayList集合,上一节我们写了一个排队类的基本操作,最终我们通过优化,将数组容量放在构造函数中进行,若未给定数组容量则默认给定一个容量,接下来我们来看看源码中初始化了一个集合到底提前做了哪些准备工作呢?

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

在我们初始化集合且类型为基本数据类型时,会有如上两个函数,一个是默认的构造函数,一个是带参数的构造函数。因为给出的例子并未包含参数,所以则是走下面一个构造函数,我们再来看看ArrayList中定义的变量,如下:

    //默认初始化容量
private static final int DEFAULT_CAPACITY = 10; //数组空实例
private static final Object[] EMPTY_ELEMENTDATA = {}; //默认空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //被操作数组
transient Object[] elementData; //数组大小
private int size; //数组最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

如上我们未给定容量时,则初始化一个空数组实例,若我们给定了容量,则走如上第一个构造函数,如果容量大于0则数组容量则为我们给定的容量,如果等于0则为空数组实例,否则抛出容量非法。接下来到了第二步,当我们添加元素2时,看看添加方法是如何操作的。

添加元素分析

//添加元素实现
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}

我们继续看看ensureCapacityInternal(size + 1)方法,此方法用来计算数组容量,看看最终方法实现,如下:

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
    //计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//当实例化集合时未给定数组容量或者指定容量为0时,则此时数组为空数组实例
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//此时minCapacity为1,通过Math.max函数将minCapacity和DEFAULT_CAPACITY(默认容量)比较返回【10】
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//当实例化时给定数组容量大于0,则直接返回添加一个元素后的容量即(size+1)
return minCapacity;
}
    //判断是否扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 若计算过后的数组容量大于数组存储长度时则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容核心实现
private void grow(int minCapacity) { //被操作数组实际容量
int oldCapacity = elementData.length; //新容量 = (实际容量 + 实际容量/2并去模)即1.5倍旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //若新容量小于数组大小则以数组大小为新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //若新容量大于定义的最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); //扩容后的新数组
elementData = Arrays.copyOf(elementData, newCapacity);
} //计算数组最大容量
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError(); //若数组大小大于定义的最大数组大小则新容量最大为整数最大值,否则为定义的最大数组大小
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

如上红色标记的是自动扩容定义的数组最大容量,这里需要解释下oldCapacity >> 1是啥意思,学校所学都还给了老师,查了资料才搞懂,这里也做个备忘录。>>在计算机表示右移,大部分情况下我们使用这种运算符比较少,但是这里为何不直接乘除呢?而且我们还看的懂些,使用左右移,运算速度快,直接乘除需要cpu计算消耗内存。刚一开始看到这个我是懵逼的,其实很简单。比如32,我们有2进制表示则为100000,怎么计算来的呢,如下:

(1 * 2 ^ 5)+(0 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 32 + 0 + 0 + 0 + 0 + 0 = 32。

好了我们知道32表示为二进制则是【100000】,那么32>>1则表示将十进制32转换为二进制后整体向右移动一位,将左边空余的补0,右边多余的剔除,如果是左移则相反(这里需注意int为32位,但是数字没那么大,所以左侧肯定全部为0,这里我们省略了哦),如下:

所以32>>1向右移动一位后如图,那么计算结果和上述第一张图一样,如下:

(0 * 2 ^ 5)+(1 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 0 + 16 + 0 + 0 + 0 + 0 = 16。

为了验证上述结果,我们通过代码来打印看看是否正确,如下:

 System.out.println( 32 >> 1);

通过如上图我们很容易得出结论:如果是右移即>>,那么用原数据除以2的位数次幂并舍去模,如果是左移即<<,那么用原数据乘以2的位数次幂。比如11>>2,通过11除以2^2,立马得出结果为2。若是11<<2,则是11*2^2,结果将是44。分析源码到这里为止,我们可得出如下结论:

若未给定初始化容量,则默认初始化容量为10且初始化默认容量的时机是在进行添加操作时。

自动扩容大小为1.5倍原始容量。

容量最大为Integer.MAX_VALUE即2147483647。

添加指定索引元素分析

上述我们只是分析完了初始化集合实例以及添加元素,接下来我们在指定索引位置添加元素看看,如下:

public static void main(String[] args) {

        ArrayList<Integer> list = new ArrayList<>();
list.add();
list.add(); //添加元素2到索引5
list.add(,);
}

依据上述我们所分析,因为在初始化集合时我们并未指定容量,所以当我们添加元素时,此时集合的容量默认为10,接下来我们在索引为5的位置添加元素2,那么是不是就可以呢?

我们以为默认容量为10,在指定索引为5插入元素不会有问题,但是结果却是抛出了异常,这说明不是以数组默认容量或提供的初始容量来作为判断依据,而是以数组实际大小来进行判断,为了证明我们的观点,我们来分析在指定索引位置插入元素的方法,如下:

//添加指定索引元素
public void add(int index, E element) { //检查索引范围,确认是否添加
rangeCheckForAdd(index); ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {

        //要添加的元素索引不能大于数组实际大小或小于0,否则抛出异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

有的人可能就问了,分析源码有什么意义或作用吗?作用太多了,一是了解背后本质原理不会出现自认为所谓的“坑”,二是通过学习并写出高质量的代码,三其他等等。我们有了对原理的了解,接下来我们就来做一个题目,如下:

public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.add(6,2);
}

因为我们知道默认初始化容量为10,所以当添加元素到11时即上述在索引6的位置插入元素2,此时将自动扩容且容量大小为15(如果还是不懂,建议再重头复习下本篇文章)。接下来我们再来分析分析trimToSize方法。

trimToSize分析

首先我们来看如下一段代码:

public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(20);
list.add(1);
list.add(2); list.add(6,2);
list.trimToSize();
}

如上我们提供初始化容量为20,但是呢结果我们实际仅仅只添加了三个元素,在数组中剩余17个元素却占着坑,所以这个时候为了解决这样的问题就引入了trimToSize方法,旨在解决如下三个问题

将集合缩减到当前集合实际存储大小

最小化集合实例的存储

当我们需要缩减集合并最小化存储时

public void trimToSize() {
modCount++;
//若数组实际大小小于数组容量时
if (size < elementData.length) {
//若数组实际大小为0时则数组为空实例,否则复制数组到当前数组大小
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

remove分析

在java中可以针对指定元素所在索引位置删除,也可以直接删除元素,下面我们首先来看看根据索引删除元素,如下:

//删除指定索引元素并返回删除元素值
public E remove(int index) {
//判断索引是否小于数组实际大小,否则抛出异常
rangeCheck(index); modCount++; //获取索引元素
E oldValue = elementData(index); //获取复制数组时要复制元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved); //将被删除的元素置为空,便于垃圾收集器回收
elementData[--size] = null; return oldValue;
}

若我们要直接元素,比如删除上述添加的元素2,此时针对删除方法尤其重载,其参数是对象,所以我们需要将元素2转换为包装类,比如如下:

//删除指定元素
public 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;
} //快速删除(本质上采用复制的方式)
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;
}

其实我们看到源码中很多操作方法内部都是采用复制的方法来进行,比如删除、添加集合等等,同时我们注意到在涉及到复制时都会存在比如上述设置为空的情况,下面我们来稍微研究下这么做的意义在哪里?

public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6); Integer[] array = list.toArray(new Integer[0]); System.arraycopy(array, 3, array, 2,
3); for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}

如上我们通过调用系统提供的复制方法模拟删除,我们删除数组中为3的元素,然后打印数组中元素,如下:

根据调用复制来看,复制的起始位置为索引3,然后将数组中元素4、5、6进行复制,但是将原有数组中的元素3、4、5进行了覆盖,但是此时元素6没有元素覆盖,所以数组中依然有6个元素,所以为了GC,我们需要将元素6设置为空,并且长度设置为5,这样才是最优代码,同样也就达到了在删除元素时elementData[--size] = null同等效果。

总结

本节我们详细分析了ArrayList源码,ArrayList的本质上是通过动态扩容一维数组来实现,同时介绍了比较常用的几个方法,当然还有比如java 8中出现的通过lambda表达式进行遍历没有再详细去一一解释,后续在学习或做项目时用到了发现有需要补充的地方,我会回过头来再进行研究,暂且到这里为止,下节我们继续学习其他集合并分析源码,感谢您的阅读,下节见。

Java入门系列之集合ArrayList源码分析(七)的更多相关文章

  1. Java入门系列之集合LinkedList源码分析(九)

    前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...

  2. Java入门系列之集合HashMap源码分析(十四)

    前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在 ...

  3. Java入门系列之集合Hashtable源码分析(十一)

    前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...

  4. Java -- 基于JDK1.8的ArrayList源码分析

    1,前言 很久没有写博客了,很想念大家,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,因为最近在看JDK的Collection,而且ArrayList源码这一块也经常被面试官问道,所 ...

  5. java学习笔记之集合—ArrayList源码解析

    1.ArrayList简介 ArrayList是一个数组队列,与java中的数组的容量固定不同,它可以动态的实现容量的增涨.所以ArrayList也叫动态数组.当我们知道有多少个数据元素的时候,我们用 ...

  6. Java集合-ArrayList源码分析

    目录 1.结构特性 2.构造函数 3.成员变量 4.常用的成员方法 5.底层数组扩容原理 6.序列化原理 7.集合元素排序 8.迭代器的实现 9.总结 1.结构特性 Java ArrayList类使用 ...

  7. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  8. Java集合干货——ArrayList源码分析

    ArrayList源码分析 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体 ...

  9. Java - ArrayList源码分析

    java提高篇(二一)-----ArrayList 一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 nul ...

随机推荐

  1. .NET 中数据访问用的 DBHelper(Sql Server) 类

    public class DBHelper { private static string DBConnectString = "Data Source=.;Initial Catalog= ...

  2. 《Java基础知识》Java final关键字:阻止继承和多态

    在 Java 中,声明类.变量和方法时,可使用关键字 final 来修饰.final 所修饰的数据具有“终态”的特征,表示“最终的”意思.具体规定如下: final 修饰的类不能被继承. final ...

  3. android开发针对小米、三星、华为8.0+系统个别型号打开应用闪退

    最近开发中有个别客户反馈新换的三星.小米或者华为手机打开应用就闪退,而且是个别型号.针对这种情况特别查阅了一些资料,原因是8.0+系统的手机不允许后台创建服务,那么怎么修改呢,请看代码: 1.修改启动 ...

  4. android开发检测用户是否使用了虚拟定位

    在应用开发中,如果有签到打卡之类的功能,你是否会遇到检测用户是否使用了虚拟定位软件来进行打卡?如果有,那么请仔细阅读这篇文章.该文章会带你认识什么是虚拟定位.什么是应用分身,以及如何通过代码来检测用户 ...

  5. SpringBoot集成Swagger2在线文档

    目录 SpringBoot集成Swagger2在线文档 前言 集成SpringBoot 登录接口文档示例 代码 效果 注解说明 总结 SpringBoot集成Swagger2在线文档 前言 不得不说, ...

  6. Python代码编写规范,你真的会吗?

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:yangjiajia123456  最近两年的工作都是和运维相关,有时 ...

  7. js基础总结04 --bom对象

    1.Bom 定义:浏览器对象模型,包含一系列与浏览器窗口交互的对象,如:Window,Location,History,Document,Screen 2.Window对象 定义:窗口对象,所有js中 ...

  8. js 对象 / json / jsonb / jsonp 区别

    一.JSON vs JS 对象 1.区别 区别 Javascript 对象 Json 含义 对象的实例 一种数据格式(序列化格式) 传输 不能传输 可以跨平台传输,轻量级 格式 1.键不加引号.加单引 ...

  9. 5分钟搞定Jenkis

    目录 什么是持续集成 Jenkins简介 Jenkins安装与启动 Jenkins插件安装 全局工具配置 自动安装 本地安装 代码上传至Git服务器 任务的创建与执行 Go项目 JAVA项目 @ 什么 ...

  10. spring cloud学习与思考——总起篇

    接下来就spring cloud( Spring Boot 2.0版本)写一个系列,一方面加深记忆理解,做个日志,另一个方面,借这个方式跟朋友们沟通交流. 1.Spring boot是Spring的一 ...