ArrayList介绍
ArrayList是一种线性数据结构,它的底层是用数组实现的,相当于动态数组。与Java中的数组相比,它的容量能动态增长。类似于C语言中的动态申请内存,动态增长内存。
当创建一个数组的时候,就必须确定它的大小,系统会在内存中开辟一块连续的空间,用来保存数组,因此数组容量固定且无法动态改变。ArrayList在保留数组可以快速查找优势的基础上,还解决了自动扩容问题。
ArrayList特点
- 快速查找:在物理内存上采用顺序存储结构,因此可根据索引快速地查找元素。
- 容量动态增长: 如下图所示,当数组容量不够用时(表1),自动创建一个比原数组容量大的新数组(表2),并将数组中的元素复制到新数组(表3),再将新的元素也放入新数组(表4),最后将新数组赋给原数组引用即可。
ArrayList继承关系
ArrayList继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口,实现了所有List接口的操作,并ArrayList允许存储null值。
除了没有进行同步,ArrayList基本等同于Vector。在Vector中几乎对所有的方法都进行了同步,但ArrayList仅对writeObject和readObject进行了同步,其它比如add(Object)、remove(int)等都没有同步。
- AbstractList提供了List接口的默认实现(个别方法为抽象方法)。
- List接口定义了列表必须实现的方法。
- 实现了RandomAccess接口:提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
- 实现了Cloneable接口:可以调用Object.clone方法返回该对象的浅拷贝。
- 实现了 java.io.Serializable 接口:可以启用其序列化功能,能通过序列化去传输。未实现此接口的类将无法使其任何状态序列化或反序列化。序列化接口没有方法或字段,仅用于标识可序列化的语义。
ArrayList的实现
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面进行具体的介绍:
私有属性
// 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
private static final int DEFAULT_CAPACITY = 10;
// 当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 保存ArrayList中数据的数组
transient Object[] elementData;
// ArrayList中实际数据的数量
private int size;
// ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;
很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。 有个关键字需要解释:transient。
Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能对象内部有一些特殊的成员变量,我们不想在序列化时保存它们。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上transient关键字。
构造函数
ArrayList提供了三种方式的构造器,可以构造一个指定初始容量的空列表、构造一个默认初始容量为10的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
// 如果initialCapacity大于0,则创建一个大小为initialCapacity的对象数组赋给elementData。
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //如果initialCapacity等于0,则将EMPTY_ELEMENTDATA赋给elementData。
this.elementData = EMPTY_ELEMENTDATA;
} else { //如果initialCapacity小于0,抛出异常(非法的容量)。
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// ArrayList构造函数。默认容量是10。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
添加/插入元素
ArrayList是基于数组实现的,当添加/插入元素的时候,如果数组容量足够大,则直接添加/插入该元素即可,
如果数组容量不够,以 add(E e)
为例,可以看到add(E e)中先调用了ensureCapacityInternal(size+1)方法,之后将元素赋给elementData[size],而后size自增。
比如,当第一次添加元素时,size为0,而调用add时将elementData[0]赋值为e,size设置为1。这时,将元素的索引赋给elementData[size],就会出现数组越界的情况。这里关键就在ensureCapacityInternal(size+1)中了。
在数组最后添加元素
当调用 add(E e)
方法向数组中添加元素时,默认是添加到数组中最后一个元素的后面。
// 添加元素e
public boolean add(E e) {
// 保证在添加元素前,数组的长度容量能满足要求,即对数组的长度容量最小要求(minCapacity)= 当前元素已占用的长度+1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
在指定位置添加元素
当调用下面这个方法以在指定索引位置,向数组中添加元素时,会先查找索引位置,然后将元素添加到索引处,最后把添加前索引后面的元素追加到新元素的后面。
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++;
}
在数组最后添加多个元素
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
调整数组容量 ensureCapacity
从上面向ArrayList中添加/插入元素的代码中,我们可以看到,每当向数组中添加元素时,在内部都要调用ensureCapacityInternal(int minCapacity)
,以去检查,添加元素后,元素的个数是否会超出当前数组的长度,如果超出,则先将数组进行扩容,再添加元素。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 当集合中还没有元素时,取10(默认集合容量)和minCapacity中的最大值作为当前需要的最小集合容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 确定ArrarList的容量
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 若当前容量不足以容纳当前的元素个数,设置 新的容量=“(原始容量x3)/2 + 1”
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
元素修改
set(int index, E element)
方法的作用是指定下标索引处的元素的值。在ArrayList的源码实现中,方法内首先判断传递的元素数组下标参数是否合法,然后将原来的值取出,设置为新的值,将旧值作为返回值返回。
public E set(int index, E element) {
// 检验索引是否合法
rangeCheck(index);
// 旧值
E oldValue = elementData(index);
// 赋新值
elementData[index] = element;
// 返回旧值
return oldValue;
}
元素读取
get(int index)
方法是返回指定下标处的元素的值。get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0)。如果所引致合法,则调用 elementData(int index)
方法获取值。在 elementData(int index)
方法中返回元素数组中指定下标的元素,并且对其进行了向下转型。
// 返回此列表中指定位置上的元素。
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
元素删除
remove(int index)
方法的作用是删除指定下标的元素。在该方法的源码中,将指定下标后面一位到数组末尾的全部元素向前移动一个单位,并且把数组最后一个元素设置为null,这样方便之后将整个数组不再使用时,会被GC,可以作为小技巧。而需要移动的元素个数为:size-index-1。
// 删除ArrayList指定位置的元素
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; // clear to let GC do its work
return oldValue;
}
ArrayList的优缺点
ArrayList的优点
- ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
- ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已。
- 根据下标遍历元素,效率高。
- 根据下标访问元素,效率高。
- 可以自动扩容,默认为每次扩容为原来的1.5倍。
ArrayList的缺点
- 插入和删除元素的效率不高。
- 根据元素下标查找元素需要遍历整个元素数组,效率不高。
- 线程不安全。
Reference
- 【数据结构】ArrayList原理及实现学习总结 - https://blog.csdn.net/jianyuerensheng/article/details/51192811
- 用大白话告诉你ArrayList的底层原理 - https://blog.csdn.net/weixin_36378917/article/details/81812210