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 ...
随机推荐
- Vmare 无法打开内核设备“\\.\VMCIDev\VMX”: 系统找不到指定的文件。您在安装 VMware Workstation 后是否进行了重新引导?的解决办法
1.使用管理员省份运行cmd:net start vmx86(切记是要用管理员身份),启动服务成功问题即可解决. 2.若1操作中启动失败,则到Vmare安装目录下搜索vmx86.sys文件,并拷贝到C ...
- native C++ 动态调用.NET DLL
关于这个问题找了好多地方,都只有第二种解决办法,可是我要返回一个字符串,没办法,继续找,最后还是在http://blogs.msdn.com/b/msdnforum/archive/2010/07/0 ...
- webpack实践(三)- html-webpack-plugin
webpack系列博客中代码均在github上:https://github.com/JEmbrace/webpack-practice <webpack实践(一)- 先入个门> < ...
- 【ES6基础】字符串扩展
4.字符串扩展 (1)for...of循环遍历. let foo = [1,2,3,4,5,6] for(let i of foo){ console.log(i); } 结果: (2)include ...
- poj 2398 Toy Storage(计算几何)
题目传送门:poj 2398 Toy Storage 题目大意:一个长方形的箱子,里面有一些隔板,每一个隔板都可以纵切这个箱子.隔板将这个箱子分成了一些隔间.向其中扔一些玩具,每个玩具有一个坐标,求有 ...
- python GUI编程tkinter示例之目录树遍历工具
摘录 python核心编程 本节我们将展示一个中级的tkinter应用实例,这个应用是一个目录树遍历工具:它会从当前目录开始,提供一个文件列表,双击列表中任意的其他目录,就会使得工具切换到新目录中,用 ...
- CTF KFIOFan: 2 Vulnhub Walkthorugh
主机扫描: ╰─ nmap -p- -A 10.10.202.152 Starting Nmap 7.70 ( https://nmap.org ) at 2019-08-29 16:55 CSTNm ...
- idea上传项目到github
1.在上传项目之前需要先在idea中确认两个配置,一个是git的执行位置,电脑上没有安装git的需要提前安装(下载git软件并且安装,非github desktop),安装之后再idea的settin ...
- CentOS 7上的进程管理
一些杂乱的基础概念 程序是一种静态的文件,躺在磁盘上.而进程则是将程序运行起来放置于内存中.因此进程就是运行中的程序,是程序运行起来的一个实例.同一个程序可以运行为多个进程/实例. 进程之间有父子关系 ...
- Appium(十):元素定位(加强版)
1. 元素定位 写完上一篇元素定位的博客,发现实用性基本为零.这几天真的烦死我了,一直在找资料,还去看了一遍appium官网文档.最后结合着selenium的定位方法,测试出几种可行的元素定位方法. ...