ArrayList
是数组的代表
概念
- 数组就是一块连续的内存空间
内存中申请一块内存空间。申请的这个空间是连续的。
特征
就是连续的,而且是固定大小的,并且里面存放的值是同一个类型的,这就是数组集合的特征。
- 内存空间连续
- 内存空间大小固定
存放内容为同一类型
ArrayList
<>
声明的类型是什么就存放什么类型- 没有
<>
声明,默认存放的是Object
类型
一维数组
声明
Java中可以使用2种方式来声明数组
dataType[] arrayRefVar
类型[] 变量名
dataTypa arrayRefVar[]
类型 变量名[]
[]
是数组的特有特征
创建
Java中数组的创建方式同样有2种
arrayRefVar = new dataType[arraySize]
- 显示的告诉程序,向
JVM
内存中申请了arraySize
长度的连续的内存空间 - 数组具体是多大,由
dataType
的类型来决定的
- 显示的告诉程序,向
dataType[] arrayRefVar = {value0, value1, value2,...,valuek}
- 隐式的去创建,大括号里面有多少个元素,对应的长度就是多少
数组说明
- Java语言中提供的数组是用来存储固定大小的同类型元素
开始下标
数组下标从 0 开始
长度
数组长度为声明的
arraySize
最后下标
数组最后的元素下标为 数组长度 -1
arraySize-1
获取元素
数组里面的下标叫索引
获取下标元素
获取数组下标为
n
的值,表达式就是arrayRefVar[n]
- 获取下标为
3
的值,arrayRefVar[3]
- 获取下标为
获取第几个元素
获取数组第
m
个的值,表达式就是arrayRefVar[m-1]
- 获取数组内第
4
个值,表达式就是arrayRefVar[3]
- 获取数组内第
List数组
- 就是
ArrayList
- 允许有重复的元素并且有先后放入次序
ArrayList
类的底层是采用动态数组进行数据管理的,支持下标访问,增删元素不方便。
1. 动态数组扩容
2. 下标直接访问
3. 增删不方便
声明
ArrayList构造方法有2个:
- 一个是带参,声明时指定对应内存空间大小;
- 一个 是无参,声明时还没有指定内存空间大小,在第一次添加元素的时候,会指定内存空间大小,默认大小为10。
指明长度
List arrList = new ArrayList(4);
对应有参构造方法源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//03、空集合创建
private static final Object[] EMPTY_ELEMENTDATA = {};
//初始化创建list
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//01、element初始化创建长度为initialCapacity的一个object类型的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity 0) {
//02、如果长度是0 就创建默认的空集合
this.elementData = EMPTY_ELEMENTDATA;
} else {
//04、否则的话就抛异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//Collection是参数;List接口和HashSet接口统统属于Collection,Map不属于Collection接口
//HashSet接口传入也可以转为ArrayList
public ArrayList(Collection<? extends E> c) {
//
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
}
- 声明的int大小,就是在内存中占用的空间大小
未知长度
//多态声明
List arrList = new ArrayList();
对应无参构造方法源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//ArrayList 的大小,就是包含的元素数量
private int size;
//2、默认实现:就是object的一个数组 Object的一维空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//数组缓冲区,Object[]数组类型的一个缓冲区
//验证了对应的 ArrayList 底层是一个数组类型的缓冲区
//3、把这object数组赋值给整个的成员变量
//transient 序列化「不是程序内序列化的概念」,实现:现在运行一个在jvm内存中运行,现在想把在jvm内运行的放在磁盘上,下次直接使用这个去弄
transient Object[] elementData;
public ArrayList() {
//1、集合底层的数组给它一个默认的实现
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
- 无参声明的情况下,只是在堆里面声明了一个地址及名称,没有真正的在栈里面申请内存空间的大小
只是声明了一个空数组,对应
size
值为0为何2个构造方法?
场景:
想要存入的数组大小已知为7,
有参
已知 数组长度,声明list的时候就声明大小
- 这样内存空间不会有浪费
比如想要数组大小是7,已经知道数组长度,
如果声明list的时候不声明大小,
那么默认在内存中list占用的空间是10个的大小空间,
这样对应有3个大小的空间其实是被浪费掉的,
所以这个时候我们就要用int参数的去声明。如果想要存入的数据长度大小是30的list数组,
不声明大小,那第一次10,第二次15,第三次22,第四次33,同样里面会有3个大小是被浪费的
无参
- 无参声明数组时不会占用内存空间
- 在第一次添加元素时,声明内存空间占用10个
总结
数组自定义时,要考虑以下几个因素:
- 数组名「name」
- 数组容量「capacity」
- 数组长度「size」
arrayList
底层是一个 数组- 有2个成员变量组成,一个是
[]
,一个是size
- 有2个成员变量组成,一个是
arrayList
数组的 初始容量是10- 如果数组不够用,会进行扩容,会1.5大小的进行扩容
- 构造函数在1.8版本有bug,在1.9版本解决掉了
size作用
- 第一个作用就是指明数组当前有多少个元素,初始化没有给值的时候size指向的是0,说明是0个
- 第二个作用,我们向数组中添加元素的时候,对应添加元素的下标位置是什么,可以用size表示
size初始值是0,对应默认的数组创建后要添加元素时,第一个size指针的位置也是0
面试题一
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//空集合创建
private static final Object[] EMPTY_ELEMENTDATA = {};
//把这object数组赋值给整个的成员变量
//transient 序列化「不是程序内序列化的概念」,实现:现在运行一个在jvm内存中运行,现在想把在jvm内运行的放在磁盘上,下次直接使用这个去弄
transient Object[] elementData;
//Collection是参数;List接口和HashSet接口统统属于Collection,Map不属于Collection接口
//HashSet接口传入也可以转为ArrayList
public ArrayList(Collection<? extends E> c) {
//
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
}
public class ArrayListDemo {
public static void main(String[] args) {
//Integer数组声明,隐士初始化
Integer[] array = {1,2};
//Integer数组转换为List
List<Integer> integerList = Arrays.asList(array);
//Object[] toArray();
Object[] objectArray = integerList.toArray();
System.out.println(objectArray.getClass() Object[].class);
//jdk8:false 有bug,不会去返回导致的,所以用jdk11已解决
// jdk11:true
//Object[] toArray();
List<Integer> list = new ArrayList<>();
System.out.println(list.toArray().getClass() Object[].class);//true
}
}
添加元素
- 向数组内顺序添加元素,复杂度是O(1)级别的,指向这个位置,直接添加就可以
数组往指定位置上添加的时候,时间复杂度O(n),数组的特点就是添加比较耗时
直接数组末尾追加
add(E e)
add()
e
:要添加的元素对象,比如66
modCount++
modCount
:列表在结构上被修改的次数- 数组内添加元素,会改变原有数组
add(e, elementData, size);
添加元素之前看下数组的内存空间是否还有地方
elementData
:初始化时声明的数组s
:size
,初始化的值为0elementData = grow()
:进行扩容
直接添加元素
如果数组的内存空间有空余,则直接 数组元素数量 「
size
」加 1, 数组长度 「elementData.length
」不变elementData[s] = e;
s
就是数组元素数量size
e
要添加的元素,不需要扩容,直接赋值
size = s + 1;
- 数组元素数量多1️
数组长度为3,第2次添加元素 object,在add之前,size=1,add添加直接 elementData[1] = object; size=2
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//列表在结构上被修改的次数
protected transient int modCount = 0;
//ArrayList 包含的元素数量 严格来说不是长度大小
private int size;
public boolean add(E e) {
//添加会修改数组
modCount++;
//01、首先确保有容量
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
//ArrayList 的大小 初始化当前数组的长度
if (s elementData.length)
//扩容操作
elementData = grow();
elementData[s] = e;
size = s + 1;
}
}
指定位置添加
add(int index, E element)
想要在一个已经创建好的数组内插入元素,首先我们需要把对应插入元素的下标的内容空出来,需要进行一个元素转移,把88然后放进去,然后size指针指到下一个要添加元素的位置,并且说明当前元素大小是多少,对应动态图如下:
rangeCheckForAdd(index);
- 先检查传入的数组下标是否有越界
- 超出则抛异常:
IndexOutOfBoundsException
{数组越界异常}
modCount++
modCount
:列表在结构上被修改的次数- 数组内添加元素,会改变原有数组
- 如果数组的内存空间有空余
arraycopy
- 数组要插入元素后的元素通过copy来进行向后移动,要插入元素的位置空出来
elementData[index] = element;
- 直接赋值要插入的元素到当前位置
- 直接 数组元素数量 「
size
」加 1, 数组长度 「elementData.length
」不变
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
扩容操作
扩容怎么操作呢?
先申请一个新的内存空间,这个时候数组想扩多大就扩多大,
然后将原来的数组内容移动到一个新的数组中,动态数组的一个动态扩容叫做一个伪扩容。栈中引用的内容只认data,所以data进行一个重新指向就可以了, 对应newData引用 就没用了。
对于我们来说看到的还是data,只不过对应的data指向了新的引用空间。
老的内存空间就干掉了,数组对应的物理地址引用,指向了新的内存空间。
s elementData.length
s
就是数组元素数量size
elementData.length
是 数组长度- 此时说明数组占用的内存空间里没有空闲空间,最后一个数组元素就在
数组长度-1
的位置上 - 这时要添加元素的话,由于原始申请的内存空间没有了,需要扩容继续申请内存空间
grow(size + 1)
- 扩容
- 底层就是数组的复制
Arrays.copyOf(elementData, newCapacity(minCapacity));
- 把原有数组所在的内存空间的元素,复制到新扩容的内容空间里面
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //直接数组最后元素添加的扩容 private void add(E e, Object[] elementData, int s) { //ArrayList 的大小 初始化当前数组的长度 if (s elementData.length) //扩容操作 elementData = grow(); elementData[s] = e; size = s + 1; } //指定元素位置添加的扩容 public void add(int index, E element) { //... final int s; Object[] elementData; if ((s = size) (elementData = this.elementData).length) elementData = grow(); //... } private Object[] grow() { return grow(size + 1); } //增加容量以确保它至少可以容纳最小容量参数指定的元素数量。 //参数:minCapacity – 所需的最小容量 private Object[] grow(int minCapacity) { //数组 = 复制一个新数组,对应要复制的数组elementData,返回的副本的长度 return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); } }
grow(int minCapacity)
:- 数组拷贝到新数组中
具体扩容逻辑
newCapacity(minCapacity)
minCapacity
:size + 1
- 扩容的最小容量是 原来数组长度+1
int oldCapacity = elementData.length;
- 扩容前数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
- 扩容后数组长度
- 旧数组长度 + 0.5倍的旧数组长度
- 每次扩容为 原数组的
1.5
倍
<< : 左移运算符,num << 1,相当于num乘以2
>> : 右移运算符,num >> 1,相当于num除以2
newCapacity - minCapacity <= 0
- 新容量 <= 最小容量
场景1:
return minCapacity;
直接返回最小容量
原来数组容量为1,则按 动态扩容 规则新数组容量为1.5;直接扩容的最小容量是2; 那就直接返回最小容量
场景2:
elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- 初始化数组,无参构造初始化数组
Math.max(DEFAULT_CAPACITY, minCapacity)
比较最大的值为最新数组的容量- 如果添加元素对应数组大小小于10的时候,直接创建一个长度为10的数组。
ArrayList
都将扩展为DEFAULT_CAPACITY
「默认数组空间直接扩容大小为10」
场景3:
minCapacity < 0
- 最小容量<0
抛异常:内存不足异常
OutOfMemoryError
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private int newCapacity(int minCapacity) {
// 旧容量 数组创建初始化的时候的容量
int oldCapacity = elementData.length;
//新容量是1.5倍旧容量 oldCapacity >> 1 oldCapacity除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新容量 <= 最小容量
if (newCapacity - minCapacity <= 0) {
//
if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//DEFAULT_CAPACITY = 10 默认初始容量
return Math.max(DEFAULT_CAPACITY, minCapacity);
//如果最小容量<0 内存不足异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//新容量 <= 最小容量 直接返回 最小容量
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
}
总结:
- 动态扩容数组1.5倍在数组长度大于10以后才真正生效。
删除元素
对应的删除,删除完对应下标元素后,需要把其它元素从后往前移动。
把数组中索引为1的位置进行删除,对应我们直接删除数据就可以。
但是数组是一个连续的内存空间,如果把1删除了对应的内存空间就不连续了,
所以要继续,把后面的数组元素往前移动,进行一个元素的转移,
所以数组删除也不快,时间复杂度也是O(n)级别的。
Objects.checkIndex(index, size);
- 先检查下对应传入的下标是否数组越界
fastRemove
- 快速删除
oldValue
返回 要删除的元素
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } private void fastRemove(Object[] es, int i) { modCount++; //新数组长度 final int newSize; //有点类似循环操作 if ((newSize = size - 1) > i) //源数组:es;源数组中的起始位置:i + 1;目标数组:es; //目标数据中的起始位置:i;要复制的数组元素的数量:newSize - i //将指定源数组中的数组从指定位置开始复制到目标数组的指定位置 System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; } }
fastRemove(Object[] es, int i)
modCount
- 数组变化,所以自增
newSize
- 删除元素后新数组长度
size - 1
i
- 要删除的元素下标
newSize > i
- 说明删除的不是最后一个元素,需要进行数组拷贝
拷贝长度为
newSize - i
数组长度是6,删除下标为3的元素,则newSize=5,数组拷贝往前移动2个(5-3)
- 最后更新数组长度
总结:
- 数组删,时间复杂度也是O(n)级别的
获取元素/修改元素
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
}
get()
方法去校验一下下标是否超出长度,超出就抛异常,没有超出就直接返回set()
方法同样也是先校验一下对应下标是否超出长度,如果超出就抛异常。如果没有就直接拿到这个下标内容,直接用传入的参数值替换掉
异常抛出情况
rangeCheckForAdd(index);
- add()方法指定index添加元素使用:
add(int index, E element)
传入下标和数组元素个数做比较,下标只有大于数组个数抛 数组越界异常,下标等于数组长度不抛异常
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
Objects.checkIndex(index, size);
- remove(),get(),set() 使用
传入下标和数组元素个数做比较,下标等于元素个数也抛数组越界异常
public static int checkIndex(int index, int length) { return Preconditions.checkIndex(index, length, null); }
public static <X extends RuntimeException> int checkIndex(int index, int length,BiFunction<String, List<Integer>, X> oobef) { if (index < 0 || index >= length) throw outOfBoundsCheckIndex(oobef, index, length); return index; }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。