ArrayList是一种以数组实现的List,与数组相比,它具有动态扩展的能力,因此也可称之为动态数组。
在ArrayList集合里面可以存储任何类型的数据, 而且它是一个顺序容器,存放的数据顺序就是和我们放入的顺序是一致的,而且它还允许我们放入null元素。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...}
ArrayList实现了List,提供了基础的添加、删除、遍历等操作。
ArrayList实现了RandomAccess,提供了随机访问的能力。
ArrayList实现了Cloneable,可以被克隆。
ArrayList实现了Serializable,可以被序列化。
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组,如果传入的容量为0时使用
*/
private static final Object[] EMPTY_ELEMENTDATA = {
};
/**
* 空数组,传传入容量时使用,添加第一个元素的时候会重新初始为默认容量大小
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
/**
* 存储元素的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 集合中元素的个数
*/
private int size;
(1)DEFAULT_CAPACITY:默认容量为10,也就是通过new ArrayList()
创建时的默认容量。
(2)EMPTY_ELEMENTDATA:空的数组,这种是通过new ArrayList(0)
创建时用的是这个空数组。
(3)DEFAULTCAPACITY_EMPTY_ELEMENTDATA:也是空数组,这种是通过new ArrayList()
创建时用的是这个空数组,与EMPTY_ELEMENTDATA
的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)
个元素。
(4)elementData:真正存放元素的地方。
(5)size:真正存储元素的个数,而不是elementData数组的长度。
为什么ArrayList的elementData数组要加上transient修饰?
由于ArrayList有自动扩容机制,所以ArrayList的elementData
数组大小往往比现有的元素数量大,如果不加transient
直接序列化的话会把数组中空余的位置也序列化了,浪费不少的空间。
ArrayList中重写了序列化和反序列化对应的writeObject
和readObject
方法,在遍历数组元素时,以 size 作为结束标志,只序列化ArrayList中已经存在的元素。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果传入的初始容量大于0,就新建一个数组存储元素
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果传入的初始容量等于0,使用空数组EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果传入的初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
public ArrayList() {
// 如果没有传入初始容量,则使用空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 使用这个数组是在添加第一个元素的时候会扩容到默认大小10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 把传入集合的元素初始化到ArrayList中
*/
public ArrayList(Collection<? extends E> c) {
// 集合转数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果c的空集合,则初始化为空数组EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
添加元素到末尾,平均时间复杂度为O(1)。
public boolean add(E e) {
// 检查是否需要扩容
ensureCapacityInternal(size + 1);
// 把元素插入到最后一位
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量为旧容量的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);
}
添加元素到指定位置,平均时间复杂度为O(n)。
public void add(int index, E element) {
// 检查是否越界
rangeCheckForAdd(index);
// 检查是否需要扩容
ensureCapacityInternal(size + 1);
// 将inex及其之后的元素往后挪一位,则index位置处就空出来了
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将元素插入到index的位置
elementData[index] = element;
// 大小增1
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
ArrayList在新增的时候为什么慢?
通过以上的源码,我们可以看出ArrayList
有指定index新增,也有直接新增的,在这之前他会有一步校验长度的判断ensureCapacityInternal
,就是说如果长度不够,是需要扩容的。
在扩容的时候,老版本的jdk和8以后的版本是有区别的,8之后的效率更高了,采用了位运算,右移一位,其实就是除以2这个操作。int newCapacity = oldCapacity + (oldCapacity >> 1);
新增后的数组容量是旧数组容量的1.5倍。
指定位置新增的时候,在校验之后的操作很简单,就是数组的copy,System.arraycopy(elementData, index, elementData, index + 1, size - index);
,为了更好的解释,这里画个图,如下:
比如有下面这样一个数组我需要在index 4 的位置去新增一个元素 a
从代码里面我们可以看到,它复制了一个数组,是从index 4 的位置开始的,然后把它放在了index 4+1 的位置
给我们要新增的元素腾出了位置,然后在index的位置放入元素a就完成了新增的操作了。
这只是在一个这么小的List里面操作,要是我去一个几百几千几万大小的List新增一个元素,那就需要后面所有的元素都复制,然后如果再涉及到扩容啥的就更慢了不是嘛。
求两个集合的并集。
/**
* 将集合c中所有元素添加到当前ArrayList中
*/
public boolean addAll(Collection<? extends E> c) {
// 将集合c转为数组
Object[] a = c.toArray();
int numNew = a.length;
// 检查是否需要扩容
ensureCapacityInternal(size + numNew);
// 将c中元素全部拷贝到数组的最后
System.arraycopy(a, 0, elementData, size, numNew);
// 大小增加c的大小
size += numNew;
// 如果c不为空就返回true,否则返回false
return numNew != 0;
}
获取指定索引位置的元素,时间复杂度为O(1)。
public E get(int index) {
// 检查是否越界
rangeCheck(index);
// 返回数组index位置的元素
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
(1)检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException
异常,如果越下界抛出的是ArrayIndexOutOfBoundsException
异常。
(2)返回索引位置处的元素;
删除指定索引位置的元素,时间复杂度为O(n)。
public E remove(int index) {
// 检查是否越界
rangeCheck(index);
modCount++;
// 获取index位置的元素
E oldValue = elementData(index);
// 如果index不是最后一位,则将index之后的元素往前挪一位
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将最后一个元素删除,帮助GC
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}
删除指定元素值的元素,时间复杂度为O(n)。
public boolean remove(Object o) {
if (o == null) {
// 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
for (int index = 0; index < size; index++)
// 如果要删除的元素为null,则以null进行比较,使用==
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
for (int index = 0; index < size; index++)
// 如果要删除的元素不为null,则进行比较,使用equals()方法
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 少了一个越界的检查
modCount++;
// 如果index不是最后一位,则将index之后的元素往前挪一位
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将最后一个元素删除,帮助GC
elementData[--size] = null; // clear to let GC do its work
}
(1)找到第一个等于指定元素值的元素;
(2)快速删除,fastRemove(int index)相对于remove(int index)
少了检查索引越界的操作。
求两个集合的交集。
public boolean retainAll(Collection<?> c) {
// 集合c不能为null
Objects.requireNonNull(c);
// 调用批量删除方法,这时complement传入true,表示删除不包含在c中的元素
return batchRemove(c, true);
}
/**
* 批量删除元素
* complement为true表示删除c中不包含的元素
* complement为false表示删除c中包含的元素
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
// 使用读写两个指针同时遍历数组
// 读指针每次自增1,写指针放入元素的时候才加1
// 这样不需要额外的空间,只需要在原有的数组上操作就可以了
int r = 0, w = 0;
boolean modified = false;
try {
// 遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 正常来说r最后是等于size的,除非c.contains()抛出了异常
if (r != size) {
// 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 将写指针之后的元素置为空,帮助GC
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
// 新大小等于写指针的位置(因为每写一次写指针就加1,所以新大小正好等于写指针的位置)
size = w;
modified = true;
}
}
// 有修改返回true
return modified;
}
(1)遍历elementData数组;
(2)如果元素在 c 中,则把这个元素添加到 elementData 数组的 w 位置并将 w 位置往后移一位;
(3)遍历完之后,w 之前的元素都是两者共有的,w 之后(包含)的元素不是两者共有的;
(4)将 w 之后(包含)的元素置为null,方便 GC 回收;
求两个集合的单方向差集,只保留当前集合中不在c中的元素,不保留在c中不在当前集体中的元素。
public boolean removeAll(Collection<?> c) {
// 集合c不能为空
Objects.requireNonNull(c);
// 同样调用批量删除方法,这时complement传入false,表示删除包含在c中的元素
return batchRemove(c, false);
}
与retainAll(Collection<?> c)
方法类似,只是这里保留的是不在c中的元素。
(1)ArrayList内部使用数组存储元素,扩容时,每次加一半的空间,ArrayList不会进行缩容。
(2)ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1)。
(3)ArrayList添加元素到尾部极快,平均时间复杂度为O(1)。
(4)ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n)。
(5)ArrayList从尾部删除元素极快,时间复杂度为O(1)。
(6)ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n)。
(7)ArrayList支持求并集,调用addAll(Collection<? extends E> c)
方法即可。
(8)ArrayList支持求交集,调用retainAll(Collection<? extends E> c)
方法即可。
(7)ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)
方法即可。
文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib
文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang
文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些
文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器
文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距
文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器
文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn
文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios
文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql
文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...
文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120
文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数