浅谈ArrayList

  ArrayList类又称动态数组,同时实现了Collection和List接口,其内部数据结构由数组实现,因此可对容器内元素实现快速随机访问。但因为ArrayList中插入或删除一个元素需要移动其他元素,所以不适合在插入和删除操作频繁的场景下使用。

  ArrayList的容量可以随着元素的增加而自动增加,因此不用担心ArrayList容量不足的问题。

  ArrayList是非线程安全的。

  接下来,我们将解析ArrayList的构造方法,在看构造方法之前,我们先来明确一下ArrayList源码中的一些概念。这些变量和对象大家先记住就好了,后面会看到它们的用途。

// 默认的容量大小(常量)
private static final int DEFAULT_CAPACITY = 10; // 定义的空数组(final修饰,大小固定为0)
private static final Object[] EMPTY_ELEMENTDATA = {}; // 定义的默认空容量的数组(final修饰,大小固定为0)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 定义的不可被序列化的数组,实际存储元素的数组
transient Object[] elementData; // 数组中元素的个数
private int size;

  ArrayList有三种构造方法:

    1.无参的构造方法

    2.根据传入的数值大小,创建指定长度的数组

    3.通过传入Collection元素列表进行生成

1.无参的构造方法

// 无参的构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

  可以看出来,当我们直接创建ArrayList时,elementData被赋予了默认空容量的数组。注意,因为默认空容量数组是被final修饰的,此时ArrayList数组是空的、固定长度的,也就是说其容量此时是0,元素个数size为默认值0。

2.根据传入的数值大小,创建指定长度的数组

// 传容量的构造方法
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);
}
}

  当initialCapacity > 0时,会在堆上new一个大小为initialCapacity的数组,然后将其引用赋给elementData,此时ArrayList的容量为initialCapacity,元素个数size为默认值0。

  当initialCapacity = 0时,elementData被赋予了默认空数组,因为其被final修饰了,所以此时ArrayList的容量为0,元素个数size为默认值0。

  当initialCapacity < 0时,会抛出异常。

3.通过传入Collection元素列表进行生成

// 传入Collection元素列表的构造方法
public ArrayList(Collection<? extends E> c) {
// 将列表转化为对应的数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 此处见下面详细解析
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 赋予空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}

  传入Collection元素列表后,构造方法首先会将其转化为数组,将其索引赋给elementData。如果此数组的长度为0,会重新赋予elementData为空数组,此时ArrayList的容量是0,元素个数size为0。

  如果数组的长度不为0,会跟新size的大小为其长度,也就是元素的个数,然后执行里面的程序。大家对里面的代码可能不理解,让我们等会看下面解析。执行完后此时ArrayList的容量为传入序列的长度,也就是size的大小,同时元素个数也为size,也就是说,此时ArrayList是满的。

  让我们来看看下面的代码,然后再去理解上面 if 语句的代码:

public class Test {
public static void main(String[] args) {
// 1.创建Student对象数组
Student[] students = new Student[] {
new Student("小明", 18),
new Student("小李", 19),
new Student("小张", 21)
}; // 2.将其赋值给Object对象数组
Object[] objects = students; // 3.执行if语句前,打印数组的class
System.out.println("执行前:" + objects.getClass()); // 4.执行上面的代码
if (objects.getClass() != Object[].class) {
objects = Arrays.copyOf(objects, objects.length, Object[].class);
} // 5.执行if语句后,打印数组的class
System.out.println("执行后:" + objects.getClass());
}
}

  程序的运行结果如下:

  可以看到,对象数组也是有.class的,其中含有所存储元素的类型,而上面的那段代码的作用就是将原对象数组的数组类型转化为Object对象数组的数组类型,以便更好的存储。

ArrayList的扩容机制

  当我们探讨扩容时,肯定要从ArrayList的add方法走起,让我们来看看吧。

public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}

  这是最基本的add方法,当然,也是可以来说明问题的。可以看到,此add方法的参数就是一个被加元素,moCount是记录ArrayList被修改的次数的,可以不用管。然后是另一个add方法,所传的值是被加元素、当前数组和当前数组的元素个数,让我们来看看这个add方法吧。

private void add(E e, Object[] elementData, int s) {
// 判断元素个数是否等于当前容量
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}

  首先,它判断了元素个数是否等于当前的容量,也就是判断当前数组空间是不是满的,如果二者相等,则当前空间是满的,就需要扩容了,grow函数就是扩容函数了,扩容后再将被加元素加到数组中。

  下面我们来看看grow函数是什么样子的:

private Object[] grow() {
return grow(size + 1);
}

  它里面调用了一个带参的grow函数,参数是当前元素个数+1,也就是当前容量+1。返回的是这个函数的返回值,让我们进一步研究。

private Object[] grow(int minCapacity) {
// 获取老容量,也就是当前容量
int oldCapacity = elementData.length;
// 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
// 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,创建新数组
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}

  首先,它是记录了一下老容量的大小,然后再进行下面的操作。

  如果当前容量大于0,或者当前数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面说明过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的长度固定为0的数组,在三个构造方法中,只有无参构造方法中elementData被赋予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说,这个if语句中不会处理用默认无参构造方法创建的数组的初始扩容情况,那么其余的情况都是由此if语句来处理的。

  我们来看一下if里面的操作,先创建一个新的数组,然后将旧数组拷贝到新数组并赋给elementData返回。ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity+max(minimum growth, preferred growth)的数组。minCapacity是传入的参数,我们上面看过,它的值是当前容量(老容量)+1,那么minCapacity - oldCapacity的值恒为1,minimum growth的值恒为1;oldCapacity >> 1的功能是将oldCapacity 进行位操作,右移一位,也就是减半,preferred growth的值为oldCapacity 的一半。

  当oldCapacity 为0时,右移后还是0,也就是说此时扩容的大小为0+max(1,0)=1,容量从0扩展到1,那么什么时候是这种情况呢?当传容量的构造方法传入的是0时,elementData被赋予的是EMPTY_ELEMENTDATA,数组容量为0,此时添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。当传Collection元素列表的构造方法被传入空列表时,elementData被赋予的是EMPTY_ELEMENTDATA,数组容量为0,此时添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。

  当oldCapacity 大于0时,新创建的数组大小是老容量+老容量的一半,也就是老容量的1.5倍,每次扩容到原来的1.5倍。

  if之外就剩一种情况了,也就是用默认无参构造方法创建的数组的初始扩容情况。此时的容量为0,添加一个元素时会创建一个新的数组,其大小为max(DEFAULT_CAPACITY, minCapacity),我们从上面的源码变量信息中可得知DEFAULT_CAPACITY的值为10,而minCapacity的值为1,所以添加一个元素时,max(DEFAULT_CAPACITY, minCapacity)的值必为10。也就是说,当我们用默认无参构造方法创建的数组在添加元素前,ArrayList的容量为0,添加一个元素后,ArrayList的容量为10。

总结一下

ArrayList的特点:

  1.ArrayList的底层数据结构是数组,所以查找遍历快,增删慢。

  2.ArrayList可随着元素的增长而自动扩容,正常扩容的话,每次扩容到原来的1.5倍。

  3.ArrayList的线程是不安全的。

ArrayList的扩容:

  在两种情况下需要扩容:

  第一种情况,当ArrayList的容量为0时,此时添加元素的话,需要扩容。什么时候容量可能为0呢?当然是刚被创建的时候,三种构造方法创建的ArrayList在扩容时略有不同:

    (1)无参构造,创建ArrayList后容量为0,添加第一个元素后,容量变为10,此后若需要扩容,则正常扩容。

    (2)传容量构造,当参数为0时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。

    (3)传列表构造,当列表为空时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。

  第二种情况,当ArrayList的容量大于0时,此时添加元素若需要扩容的话,正常扩容,每次扩容到原来的1.5倍。

浅谈 ArrayList 及其扩容机制的更多相关文章

  1. 浅谈ArrayList

    浅谈ArrayList 废话不多说(事实是不会说),让我们直接进入正题 首先讲一讲最基本的ArrayList的初始化,也就是我们常说的构造函数,ArrayList给我们提供了三种构造方式,我们逐个来查 ...

  2. ArrayList的扩容机制

    一.ArrayList的扩容机制 1.扩容的计算方式是向右位移,即:newSize = this.size + (this.size>>1).向右位移,只有在当前值为偶数时,才是除以2:奇 ...

  3. 关于ArrayList的扩容机制

    关于ArrayList的扩容机制 ArrayList作为List接口常用的一个实现类,其底层数据接口由数组实现,可以保证O(1) 复杂度的随机查找, 在增删效率上不如LinkedList,但是在查询效 ...

  4. 浅谈Java的反射机制和作用

    浅谈Java的反射机制和作用 作者:Java大师 欢迎转载,转载请注明出处 很多刚学Java反射的同学可能对反射技术一头雾水,为什么要学习反射,学习反射有什么作用,不用反射,通过new也能创建用户对象 ...

  5. 浅谈:Redis持久化机制(一)RDB篇

    浅谈:Redis持久化机制(一)RDB篇 ​ 众所周知,redis是一款性能极高,基于内存的键值对NoSql数据库,官方显示,它的读效率可达到11万次每秒,写效率能达到8万次每秒,因为它基于内存以及存 ...

  6. 浅谈:Redis持久化机制(二)AOF篇

    浅谈:Redis持久化机制(二)AOF篇 ​ 上一篇我们提及到了redis的默认持久化方式RDB,是一种通过存储快照数据方式持久化的机制,它在宕机后会丢失掉最后一次更新RDB文件后的数据,这也是由于它 ...

  7. 【数组】- ArrayList自动扩容机制

    不同的JDK版本的扩容机制可能有差异 实验环境:JDK1.8 扩容机制: 当向ArrayList中添加元素的时候,ArrayList如果要满足新元素的存储超过ArrayList存储新元素前的存储能力, ...

  8. 浅谈C语言中断处理机制

    一.中断机制 1.实现中断响应和中断返回 当CPU收到中断请求后,能根据具体情况决定是否响应中断,如果CPU没有更急.更重要的工作,则在执行完当前指令后响应这一中断请求.CPU中断响应过程如下:首先, ...

  9. Java ArrayList自动扩容机制

    动态扩容 1.add(E e)方法中 ①  ensureCapacityInternal(size+1),确保内部容量,size是添加前数组内元素的数量 ②  elementData[size++] ...

随机推荐

  1. 第一篇Scrum冲刺博客

    目录 一.Alpha 阶段认领的任务 二.明日成员的任务安排 三.整个项目预期的任务量 四.敏捷开发前的感想 五.团队期望 一.Alpha 阶段认领的任务 陈起廷 任务 预计时间 日记天气.心情选择 ...

  2. Camera学习--光源

    进入CV 领域,视频图像的成像,最前端的camera,camera的sensor 以及影响成像质量的光源,噪声等因素是绕不开的问题. 那么今天就从成像的光源说起. 标准光源(Standard Ligh ...

  3. Java数据结构——二叉树的遍历(汇总)

    二叉树的遍历分为深度优先遍历(DFS)和广度优先遍历(BFS) DFS遍历主要有: 前序遍历 中序遍历 后序遍历 一.递归实现DFSNode.java: public class Node { pri ...

  4. python安装wordcloud库报错

    pip install wordcloud 安装成了这样 红彤彤的一片 解决方法 https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud 下载对应版 ...

  5. dlopen代码详解——从ELF格式到mmap

    最近一个月的时间大部分在研究glibc中dlopen的代码,基本上对整个流程建立了一个基本的了解.由于网上相关资料比较少,走了不少弯路,故在此记录一二,希望后人能够站在我这个矮子的肩上做出精彩的成果. ...

  6. json对象遍历顺序问题

    对json对象遍历我们一般使用for in循环,或者Object.keys + 数组方法.在接触js以来听到过一种说法: for in 遍历顺序是不可靠的 但是在实际开发中for in 循环也是按照其 ...

  7. 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知

    乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免 ...

  8. APICloud数据云3.0 -- 让后端业务更简单

    近年来,各类移动端应用层出不穷,app.小程序已成为企业业务数字化的必然选择,围绕互联网产品的技术创新与开发者生态,正在历经行业发展的又一次革新. APICloud作为国内领先的移动应用开发平台,一直 ...

  9. .NET5.0 单文件发布打包操作深度剖析

    .NET5.0 单文件发布打包操作深度剖析 前言 随着 .NET5.0 Preview 8 的发布,许多新功能正在被社区成员一一探索:这其中就包含了"单文件发布"这个炫酷的功能,实 ...

  10. Shader之溶解效果的几种实现方法

    这里通过 “是否丢弃像素”的2种方法,写2个shader,效果是一样的,也提到了,丢弃某个像素的3种方式. 是否丢弃: 1.通过脚本控制shader变量判断当前是否丢弃像素,需要额外脚本: 2.sha ...