上大学时学过一次 现在深入了解一下
ArrayList
1、什么是ArrayList
ArrayList就是传说中的动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了如下一些好处:
- 动态的增加和减少元素
- 实现了ICollection和IList接口
- 灵活的设置数组的大小
大家知道,数组是静态的,数组被初始化之后,数组长度就不能再改变了。ArrayList是可以动态改变大小的。那么,什么时候使用Array(数组),什么时候使用ArrayList?
答案是:当我们不知道到底有多少个数据元素的时候,就可使用ArrayList;如果知道数据集合有多少个元素,就用数组。
2.ArrayList源码解析
基本参数
/**默认初始容量*/
private static final int DEFAULT_CAPACITY = 10;
/**用于空实例的共享空数组实例*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**共享的空数组实例,用于默认大小的空实例。我们将此与EMPTY_ELEMENTDATA区别开来,以了解添加第一个元素时需要添加多少*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**存储元素的数组,在添加第一个元素时将会扩充至10*/
transient Object[] elementData;
/**ArrayList内的元素个数*/
private int size;
可以看到ArrayList内定义了两个空数组,和一个值为10的参数(注释表示初始容量,但其实是分情况的),数组长度不一定是等于size的,size是实际元素个数的数量。
1.构造函数
第一种:带初始容量的构造函数
构造一个具有指定初始容量的空列表
initialCapacity 列表的初始容量
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//如果初始容量大于0
this.elementData = new Object[initialCapacity];
//elementData赋值为一个初始容量为initialCapacity的Object数组
} else if (initialCapacity == 0) {
//如果初始容量等于0
this.elementData = EMPTY_ELEMENTDATA;//elementData赋值为空实例数组
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
第二种:不带参的构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
//赋值为已创建的空数组 以前老版本的JDK是一个大小为10的空数组的,现在修改为空数组
}
第三种:带集合的构造函数
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;
}
}
这里可以看到,不管是哪一种构造函数,在没有设置初始容量的时候,或者设置的初始容量为0的时候,都给数据持有对象elementData赋值为了空数组。
2.add方法
ArrayList无参构造方法默认是一个空数组,其实ArrayList的容量是在调用add方法时初始化的。(以前版本是初始时默认为10,现在改为空数组)
add(E e) 方法
public boolean add(E e) {
// 判断是否需要扩容
ensureCapacityInternal(size + 1);
// 将新元素追加到相应的数组中
elementData[size++] = e;
return true;
}
ensureCapacityInternal(int minCapacity)方法判断是否需要扩容
// minCapacity = size + 1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组为空,则所需容量就是默认容量
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);
}
在grow()方法中,可以看出每次扩容后的长度是当前数组长度的1.5倍,其实质是将原有数组内容复制到一个长度为newCapacity的新数组中。
// 数组默认的最大长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
// 调用Arrays.copyOf()方法,将elementData数组复制到一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
3.add(int index, E element)方法
// 将元素添加到指定位置,扩容机制与add(E e)方法一致。
public void add(int index, E element) {
// 判断index是否越界
rangeCheckForAdd(index);
// 是否扩容
ensureCapacityInternal(size + 1); // Increments modCount!
/*
* System.arraycopy()是比较重要的一个步骤,
* 其实质是将elementData数组中index位置(包含index)以后的数据全部向后移动一个位置,
* 从而用 elementData[index] = element 将index位置元素替换为待添加元素。
*/
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
4.addAll()方法
// 将指定集合中的元素添加到集合中
public boolean addAll(Collection<? extends E> c) {
// 转数组
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
// 将c中所有元素追加到集合尾部
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 将指定集合中的元素添加到集合中的指定位置
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
// 需要移动元素的个数
int numMoved = size - index;
// 先将待添加数据所需空间空出,然后将指定集合数据从指定位置开始添加
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
这里有一个问题 ,如果addall()时,size直接大于当前的容量,那么当前容量扩展,那么就进行扩容乘1.5倍!
add(E e)方法 添加元素到末尾,平均时间复杂度为O(1)。
add(int index, E element)方法 添加元素到指定位置,平均时间复杂度为O(n)。
get(int index)方法
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];
}
检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是ArrayIndexOutOfBoundsException异常。
remove(int index)方法
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) 可以看到,ArrayList删除元素的时候并没有缩容。
remove(Object o)方法
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
}
删除指定元素值的元素,时间复杂度为O(n)。
(1)ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次加一半的空间,ArrayList不 会进行缩容;
(2)ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);
(3)ArrayList添加元素到尾部极快,平均时间复杂度为O(1);
(4)ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);
(5)ArrayList从尾部删除元素极快,时间复杂度为O(1);
(6)ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);
ArrayList缩容
ArrayList没有自动缩容机制。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。ArrayList的缩容,需要我们自己手动去调用trimToSize()方法,达到缩容的目的。
ArrayList的remove只是通过数组部分元素左移、最后一个元素置为null同时size自减来实现删除操作。实际上并未对elementData进行缩容。
可以使用trimToSize()方法对elementData进行缩容。
modCount作用
这个成员变量记录着集合的修改次数,也就每次add或者remove它的值都会加1。在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操 作,这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。在AbstractList中,使用了一个简单的机制来规避这些风险。 这就是modCount和expectedModCount的作用所在