ArrayList集合特点及源码分析

ArrayList是List接口的实现类

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

继承了AbstractList类,实现了Cloneable克隆接口、Serializable序列化接口、RandomAccess随机访问接口、List接口

特点:底层使用数组实现的 查询效率高,增删慢,线程不安全、允许为空、可重复

public static void main(String[] args) {
List<String> list=new ArrayList<String>();
boolean bool=list.add("ylc");//Collection接口添加元素
list.add(1,"ww");//根据索引添加元素
String el=list.get(1);//根据索引获取元素
list.size();//获取元素个数
list.remove(2);//根据元素位置删除
list.remove("ylc");//删除指定元素
list.set(1,"yy");//替换元素
list.clear();//清空集合
list.isEmpty();//判断元素是否为空
list.contains("ylc");//判断集合是否包含某个元素
list.indexOf("ylc");//查找所诉中所在的位置
list.lastIndexOf("ylc");//元素最后出现的索引位置
Object[] objects=list.toArray();//把集合转化为object数组
for (int i = 0; i < objects.length; i++) {
String str=(String) objects[i];
System.out.println(str);
}
String[] strings=list.toArray(new String[list.size()]);//把集合转化为指定类型数组
List<String> list2=new ArrayList<String>();
list2.add("s");
list.addAll(list2);//集合合并操作
list.retainAll(list2);//集合交集操作 list存储交集内容
list.removeAll(list2);//删除list中含有list2集合的元素
}

ArrayList源码分析

成员变量

private static final int DEFAULT_CAPACITY = 10;//数组默认长度
private static final Object[] EMPTY_ELEMENTDATA = {};//给定一个空数组
transient Object[] elementData;//存储ArrayList元素的临时数组 不会被存到磁盘
private int size;//记录数组中元素的个数
protected transient int modCount = 0; // 集合数组修改次数的标识(fail-fast机制)

transient关键字对于不想进⾏序列化的字段,使⽤ transient 关键字修饰

JDK7中,只要创建ArrayList数组,就会默认创建一个长度为10的空数组。

JDK8中,做了一个延迟加载,在创建ArrayList数组时,创建一个长度为0的空数组,只有在用到这数组才会对长度进行改变,做了一个延迟加载

构造函数

里面包含三种构造函数

1.无参构造函数

初始化数组时默认赋值一个空数组

//无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//默认为0
}

2.int类型的构造函数

如果传入数值大于0就创建指定容量大小的数组,数值为0为空对象数组,否则抛出异常

//带容量大小的构造函数
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);
}
}

3.集合类型构造函数

把传入的集合变成数组,赋值给elementData

集合不为空,传入数组类型转为object的话赋值给elementData

否则就直接复制到elementData中

    public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();//变成数组
if ((size = a.length) != 0) {//集合不为空的话
if (c.getClass() == ArrayList.class) {//判断与ArrayList类型是否一致
elementData = a;//赋值
} else {
elementData = Arrays.copyOf(a, size, Object[].class);//否则直接复制到数组中
}
} else {
elementData = EMPTY_ELEMENTDATA;//设置为空数组
}
}

增加方法

add(E e)方法

 public boolean add(E e) {
ensureCapacityInternal(size + 1); //当size=0时
elementData[size++] = e;//Size还是为0,给为0的size赋值
return true;
}
//确保集合数组内部容量
private void ensureCapacityInternal(int minCapacity) {//1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//返回10
}
//确保显式容量
private void ensureExplicitCapacity(int minCapacity) {//minCapacity=10
modCount++;
if (minCapacity - elementData.length > 0)// 10-0>0 判断扩容关键代码 当第11个数组加入时执行grow代码
grow(minCapacity);//10
} private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //计算数组容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//0=0
return Math.max(DEFAULT_CAPACITY, minCapacity);// DEFAULT_CAPACITY=10,minCapacity=1 返回最大值作为数组容量
}
return minCapacity;
} //扩容方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;//0
int newCapacity = oldCapacity + (oldCapacity >> 1); //数组长度加上位移运算(数组长度的一半)每次扩容增加1.5倍 0=0+0
if (newCapacity - minCapacity < 0)//0-10<0
newCapacity = minCapacity; //newCapacity=0
if (newCapacity - MAX_ARRAY_SIZE > 0)//10-MAX_ARRAY_SIZE<0
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);//复制操作 把一个长度为10复制到空的数组中,生成了一个新的长度为10的空数组
}

Size默认是0,add方法给Size加上1,并传入ensureCapacityInternal方法,其中ensureCapacityInternal方法中又调用了ensureExplicitCapacity方法,并传入了两个参数,第一个是elementData代表一个为空的数组,第二个是数组个数。在calculateCapacity方法中,有个if判断如果数组大小等于默认大小,就返回其中最大的数值,默认数组大小为10的话,DEFAULT_CAPACITY也等于10,所以返回的是10。把10传入ensureExplicitCapacity方法中,再把10 传入grow方法中,生成了一个新的长度为10的空数组。ensureCapacityInternal方法执行完毕。add方法Size依然为0,给为下标为0索引赋值,新增一个成功。

扩容模拟

当添加第11个元素时,add方法Size=10+1=11,传入ensureCapacityInternal方法,进入calculateCapacity方法,这时elementData=10,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA=0,不满足直接返回minCapacity=11,值11进入ensureExplicitCapacity方法内部,满足11-10>0进grow方法,grow方法赋值oldCapacity=10,newCapacity=10+10的位移一位操作=10+5=15,最后使用copyOf方法,把原来10大小的数组扩容到15。

add(int index, E element)方法

public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); //集合增加容量
//elementData:原数组 index:从原数组这个索引开始复制 elementData:目标数组 index + 1:目标数组的第一个元素 size - index:复制size-index个元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;//把当前所有数组替换
size++;//索引增加
}
//判断索引是否越界
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

这样每增加一个元素就要把当前索引复制到该元素的后面,开销很大

删除方法

remove(int index)方法

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; //集合数量减1 return oldValue;
} E elementData(int index) {
return (E) elementData[index];//返回当前索引的元素
}

进行删除前检查索引是否合理,然后记录集合被修改的次数,根据索引获取到需要删除的这个元素,然后该索引后面的元素复制到当前索引,进行覆盖,最后集合元素减去。

remove(Object o)方法

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; // 集合数量减1
}

该方法利用集合遍历找到该元素,根据索引再进行删除,fastRemove方法同remove(int index)方法类似。

removeAll(Collection<?> c)方法

删除c集合中存在的元素


public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);//判断集合是否为空
return batchRemove(c, false);
} public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
} private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)//complement=flase 如果elementData不包含c里面的某个元素
elementData[w++] = elementData[r];//不包含的记录下来! 赋值到elementData,满足就从索引0开始覆盖掉原来的数组
} finally {
//存在并发修改上面发生异常没遍历完elementData,这里就会不相等 如果不存在这里就会直接跳过
if (r != size) {
//elementData:原数组 r:从原数组这个索引开始复制 elementData:目标数组 w:目标数组的第一个元素 size - r:复制元素数量 System.arraycopy(elementData, r,elementData, w,size - r);//把没有遍历到的元素赋值到已更新元素的后面
w += size - r;//现有的数据
}
if (w != size) {//w容量是不包含的 除非集合里删除一个元素都不包含这里才会跳过
for (int i = w; i < size; i++)
elementData[i] = null;//把不包含的后面多余的数组置为空
modCount += size - w;//增加修改次数
size = w;//当前数据长度
modified = true;//修改成功
}
}
return modified;
}

场景模拟代入数值:在一个10长度的数组a中删除一个3长度的数组b,删除到一半的时候,由于多线程导致数组赋值失败进入finally代码块,此时w=3,r=6,size=10,w存入的是不包含在b数组中的元素现有3个,r是遍历到中断的索引,进入(r != size)判断不相等,arraycopy方法把后面没有遍历到的重新加进来数组,w+=size - r=7;这样w中存储的就是c集合中不包含的全部元素,再遍历整个Size集合,把不包含元素后面的容量置为空。

其他方法

indexOf(Object o)方法

获取索引

public int indexOf(Object o) {
if (o == null) {//判断对象是否为空
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))//判断相等
return i;
}
return -1;//未匹配
}

lastIndexOf(Object o)

倒序获取索引

public int lastIndexOf(Object o) {
if (o == null) {//等于空
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))//判断相等
return i;
}
return -1;//未匹配
}

set(int index, E element)方法

public E set(int index, E element) {
rangeCheck(index);//检查索引 E oldValue = elementData(index);//根据索引获取元素
elementData[index] = element;//把元素赋值给当前索引
return oldValue;
}

get(int index)方法

public E get(int index) {
rangeCheck(index);//检查索引 return elementData(index);//返回根据索引获取当前元素
}

clear方法

public void clear() {
modCount++;//修改次数+1 for (int i = 0; i < size; i++)
elementData[i] = null;//遍历赋值为null size = 0;//数组中元素的个数置为0
}

总结

  • 是一个动态数组,其容量能自动增长,但每次增删都要copy,性能不高,访问的时候通过索引是最快的
  • ArrayList线程不安全,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类

ArrayList集合底层原理的更多相关文章

  1. JAVA ArrayList集合底层源码分析

    目录 ArrayList集合 一.ArrayList的注意事项 二. ArrayList 的底层操作机制源码分析(重点,难点.) 1.JDK8.0 2.JDK11.0 ArrayList集合 一.Ar ...

  2. 深度解析HashMap集合底层原理

    目录 前置知识 ==和equals的区别 为什么要重写equals和HashCode 时间复杂度 (不带符号右移) >>> ^异或运算 &(与运算) 位移操作:1<&l ...

  3. ArrayList 从源码角度剖析底层原理

    本篇文章已放到 Github github.com/sh-blog 仓库中,里面对我写的所有文章都做了分类,更加方便阅读.同时也会发布一些职位信息,持续更新中,欢迎 Star 对于 ArrayList ...

  4. Java集合:ArrayList的实现原理

    Java集合---ArrayList的实现原理   目录: 一. ArrayList概述 二. ArrayList的实现 1) 私有属性 2) 构造方法 3) 元素存储 4) 元素读取 5) 元素删除 ...

  5. 集合总结一(ArrayList的实现原理)

    一.概述 一上来,先来看看源码中的这一段注释,我们可以从中提取到一些关键信息: Resizable-array implementation of the List interface. Implem ...

  6. 深入java8的集合:ArrayList的实现原理

    一.概述 一上来,先来看看源码中的这一段注释,我们可以从中提取到一些关键信息: Resizable-array implementation of the List interface. Implem ...

  7. ArrayList的底层实现原理

    ArrayList源码分析 1.java.util.ArrayList<E> : List 接口的大小可变数组的实现类 ArrayList 内部基于 数组 存储 各个元素. 所谓大小可变数 ...

  8. 理解java容器底层原理--手动实现ArrayList

    为了照顾初学者,我分几分版本发出来 版本一:基础版本 实现对象创建.元素添加.重新toString() 方法 package com.xzlf.collection; /** * 自定义一个Array ...

  9. 红黑树规则,TreeSet原理,HashSet特点,什么是哈希值,HashSet底层原理,Map集合特点,Map集合遍历方法

    ==学习目标== 1.能够了解红黑树 2.能够掌握HashSet集合的特点以及使用(特点以及使用,哈希表数据结构) 3.能够掌握Map集合的特点以及使用(特点,常见方法,Map集合的遍历) 4.能够掌 ...

随机推荐

  1. POJ3625Building Roads

    Building Roads Description Farmer John had just acquired several new farms! He wants to connect the ...

  2. mybatis一对多联表查询的两种常见方式

    1.嵌套结果查询(部分代码如下) sql语句接上: 注释:class表(c别名),student表teacher(t别名)teacher_id为class表的字段t_id为teacher表的字段,因为 ...

  3. PHP中环境变量的操作

    在 PHP 中,我们可以通过 phpinfo() 查看到当前系统中的环境变量信息(Environment).在代码中,我们也可以通过两个函数,查看和修改相应的环境变量信息. getenv() 获取环境 ...

  4. markdown写作系统

    markdown 电脑本地使用typora,可保存为md文件直接上传到有道云笔记中 直接使用博客园做为图床,可以存为草稿,然后把复制连接 有道云笔记 粘贴博客园的图片连接

  5. python学习笔记(十五)-unittest单元测试的一个框架

    unittest 单元测试的一个框架什么框架 一堆工具的集合. TestCase TestSuite 测试套件,多个用例在一起 TestLoader是用来加载TestCase到TestSuite中的 ...

  6. 定要过python二级 选择题第四套

    1. 2. 3. 4. 5. 6. python用于人工智能 7. 8. 9. 10. 11. 12. 13. 14. 15. 16.

  7. Gitee自动化部署python脚本

    一.前期准备 1.1 安装环境 1.安装python3 2.打开命令行安装selenium pip install selenium 二.python代码 2.1 源码 #!/usr/bin/pyth ...

  8. 鸿蒙内核源码分析(线程概念篇) | 是谁在不停的折腾CPU? | 百篇博客分析OpenHarmony源码 | v21.06

    百篇博客系列篇.本篇为: v21.xx 鸿蒙内核源码分析(线程概念篇) | 是谁在不断的折腾CPU | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...

  9. 鸿蒙内核源码分析(内存主奴篇) | 皇上和奴才如何相处 | 百篇博客分析OpenHarmony源码 | v10.04

    百篇博客系列篇.本篇为: v10.xx 鸿蒙内核源码分析(内存主奴篇) | 皇上和奴才如何相处 | 51.c.h .o 前因后果相关篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 ...

  10. CF5E-Bindian Signalizing【单调栈】

    正题 题目链接:https://www.luogu.com.cn/problem/CF5E 题目大意 圆上有\(n\)个山,两个山之间可以看到当且仅当它们之间的两条弧中有一条满足所有山都不高于它们两个 ...