Java入门系列之集合ArrayList源码分析(七)
前言
上一节我们通过排队类实现了类似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源码分析(七)的更多相关文章
- Java入门系列之集合LinkedList源码分析(九)
前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...
- Java入门系列之集合HashMap源码分析(十四)
前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在 ...
- Java入门系列之集合Hashtable源码分析(十一)
前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...
- Java -- 基于JDK1.8的ArrayList源码分析
1,前言 很久没有写博客了,很想念大家,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,因为最近在看JDK的Collection,而且ArrayList源码这一块也经常被面试官问道,所 ...
- java学习笔记之集合—ArrayList源码解析
1.ArrayList简介 ArrayList是一个数组队列,与java中的数组的容量固定不同,它可以动态的实现容量的增涨.所以ArrayList也叫动态数组.当我们知道有多少个数据元素的时候,我们用 ...
- Java集合-ArrayList源码分析
目录 1.结构特性 2.构造函数 3.成员变量 4.常用的成员方法 5.底层数组扩容原理 6.序列化原理 7.集合元素排序 8.迭代器的实现 9.总结 1.结构特性 Java ArrayList类使用 ...
- java集合系列之ArrayList源码分析
java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...
- Java集合干货——ArrayList源码分析
ArrayList源码分析 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体 ...
- Java - ArrayList源码分析
java提高篇(二一)-----ArrayList 一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 nul ...
随机推荐
- webpack实践(四)- html-webpack-plugin
webpack系列博客中代码均在github上:https://github.com/JEmbrace/webpack-practice <webpack实践(一)- 先入个门> < ...
- 优先队列与TopK
一.简介 前文介绍了<最大堆>的实现,本章节在最大堆的基础上实现一个简单的优先队列.优先队列的实现本身没什么难度,所以本文我们从优先队列的场景出发介绍topK问题. 后面会持续更新数据结构 ...
- Sql将一列数据拆分为多行显示的两种方法
原始数据与期望结果有表tb, 如下:id value----------- -----------1 aa,bb2 aaa,bbb,ccc欲按 ...
- SpringCloud+Eureka+Feign+Ribbon的简化搭建流程和CRUD练习
作者:个人微信公众号:程序猿的月光宝盒 环境:win10--idea2019--jdk8 1.搭建Eureka服务模块 1.1 新建eureka服务模块(Sping Initializr) 取名为eu ...
- JS---案例:简单轮播图
案例:简单轮播图 div叫盒子,里面包了2个小盒子,一个是inner,一个是square inner的div是放ul,里面有li,a,和图片 square的div里面放span,是轮播图的小点 < ...
- 利用zabbix监控RocketMQ
根据需求,监控三个指标:MQ进程.自定义监控项订阅组的未消费值Diff Total和TPS. 创建MQ 状态的监控模板,进程监控利用zabbix自带的模板: 监控订阅组的Diff Total和TPS ...
- 学习SQL注入---1
开始接触SQL注入了,最开始根据网上的思路做了两道注入的题,但对于SQL注入如何实现,怎么一个流程还是不理解.后来,在网上查找了很多资料,现在一点点去理解. 1.利用sqlmap注入的时候,不是所有页 ...
- CentOS 7上的主机名设置和基本网络管理
主机名 CentOS 6 查看. # hostname 设置. # hostname NEW_NAME 设置完成后,xshell的会话中不会显示NEW_NAME,可通过重新登录会话来显示.不过实际上我 ...
- OpenLDAP的docker版安装
分为服务器和client的web版. ldap.sh #!/bin/bash -e docker run --name ldap-service -- docker run --name phplda ...
- 一起学Spring之基础篇
本文主要讲解Spring的基础环境搭建以及演变由来,仅供学习分享使用,如有不足之处,还请指正. 什么是Spring ? Spring是一个开源框架,用来处理业务逻辑层和其他层之间的耦合问题.因此Spr ...