深入理解系列之JAVA数据结构(1)——ArrayList
https://blog.csdn.net/u011552404/article/details/79833914
1、 ArrayList是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。提供了相关的添加、删除、修改、遍历等功能。
2、ArrayList实现了RandmoAccess接口,即提供了随机访问功能。在ArrayList中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆。ArrayList实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
3、和Vector不同,ArrayList中的操作不是线程安全的!但是Vector的线程安全降低了数据操作的效率,所以一般情况下已经不建议使用!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList
说明
关于ArrayList的底层源码如构造函数、增删改查什么的,许多博客也都进行详细的概述!所以这里就不再一一赘述,详细参见:https://blog.csdn.net/fighterandknight/article/details/61240861,这里我只对关键的问题作出解释!为了便于下面的引入源码时的一些说明,首先贴出类参数:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
// 序列化id
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData;
// 当前数组长度
private int size;
// 数组最大长度
private static final int MAX_ARRAY_SIZE = 2147483639;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
问题一、 ArrayList传入集合的构造器是如何实现的?##
ArrayList一共有三种构造方法,分别是
public ArrayList()
public ArrayList(int initialCapacity)
public ArrayList(Collection<? extends E> c)
- 1
- 2
- 3
其中第一种方法默认构造数组大小是0,只有在添加第一个数据的时候才自动扩容至默认大小10;第二种构造器是自定义默认大小;第三种构造器即传入集合进行初始化,因为ArrayList的底层是数组实现,所以实际的初始化原理是首先把集合转变成Array类然后再通过深拷贝的方法初始化ArrayList!看源码:
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
其中,有人纠结Arrays.copy是深拷贝还是浅拷贝,我们首先看源码:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
可以看到源码是在生成了一个新的对象后,通过系统调用进行拷贝的,即产生了一个新的拷贝数组对象
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
- 1
original - 源数组。
0 - 源数组中的起始位置。
copy - 目标数组。
0 - 目标数据中的起始位置。 Math.min(original.length, newLength) - 要复制的数组元素的数量。
如果还是有疑问,我们可以测试:
我们可以清晰的看到,当复制数组改变元素后,并不影响原先数组,所以复制的数组是一个新的对象,进行的是深拷贝!
问题二:扩容机制是什么?
扩容通常发生在添加元素的时候,发现超过当前数组大小(起初默认是10)的时候。其中默认扩容大小是1.5倍。由于ArrayList底层的数组本身并不是动态的,所以扩容之后并不是直接在数组后添加空位置,进而赋值,而是重新深拷贝一个数组长度为当前数组的1.5倍的新数组,然后再添加新的元素!其中扩容源码如下:
private void grow(int arg0) {
int arg1 = this.elementData.length;
int arg2 = arg1 + (arg1 >> 1);
if (arg2 - arg0 < 0) {
arg2 = arg0;
}
if (arg2 - 2147483639 > 0) {
arg2 = hugeCapacity(arg0);
}
this.elementData = Arrays.copyOf(this.elementData, arg2);
}
if (arg2 - 2147483639 > 0) {
arg2 = hugeCapacity(arg0);
}
this.elementData = Arrays.copyOf(this.elementData, arg2);
}
我们可以发现,新的数组大小的是arg2=arg1+(arg1>>1),即1.5倍,注意采用位移比乘除效率更高!然后还是调用Arrays.copy函数进行深拷贝!
问题三、为什么增删操作的效率低?
这个问题,需要分开来讲,举例只讲插入的话:
1、如果只是插入尾节点且没有空间足够,其实直接赋值就可以,和链表的复杂度一样;
但是如果以下两种情况:
1、超过当前空间(不论是尾插还是中间插入),则需要扩容!因为上文已经说过,扩容会导致深拷贝一个新的数组,那么这个复杂度就是n了!
2、没有超过当前空间,但是在中间插入,假设插入位置为index,则插入操作要使得index后的数据全部往后移动一格——需要注意的是,这个移动一格还是通过深拷贝来实现的:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
这个时候,复杂度其实是size-index!删除操作也是这个原理,同样需要拷贝!
问题四、sublist方法中返回的视图是什么意思?
视图是个内部类,可以访问ArrayList的数据!视图代表是原数组对象的偏移索引,也就是说二者指向同一个对象,只不过表征的数组的起始点不同罢了!所以需要注意的是,如果改变了返回的视图的某个索引位的值,原数组也会改变;但是改变原数组的话,将会使得当前的的视图失效!并抛出异ConcurrentModificationException 看源码:
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
- 1
- 2
- 3
- 4
可以看到,视图虽然是一个新的对象,但是实际数据是通过持有原数组并加上偏移参数表征的,那么改变视图数据自然改变的就会原数组!
问题五、为什么会抛出ConcurrentModificationException异常?
上文讲了获取视图后,如果更改原数组将会抛出ConcurrentModificationException异常,但是这个异常是什么意思呢?ArrayList继承至AbstractList,在AbstractList中有一个参数叫modCount,这个参数在子类包括ArrayList中随处可见:记录每次增删操作的次数,即每变更一次数组,就自加1操作。本身这个也不会出现什么异常,问题就是在sublist视图方法中每次变更操作之前,函数会做一个检查,拿sublist的set函数举例:
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这里有一个checkForComodification()函数,我们可以进去看看:
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
- 1
- 2
- 3
- 4
上文已经说过,sublist对象是个内部类,可以访问ArrayList数据,所以this.modCount代表的是内部类(即自己本身)的数据,而这个数据是在第一次生成内部类对象的时候(即生成sublist实例的时候)直接获取的外部类ArrayList的参数大小,这样外部类变更原数组的时候导致ArrayList.this.modCount加1,但是this.modCount还是生成实例的时候大小,于是将导致不一致,进而抛出异常!抛出这个异常的还有一个方法,即生成迭代器Iterator的时候,在外部类变更数据的后,继续执行迭代器的next函数,也会导致抛出异常!源码:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)