ArrayList
ArrayList定义及功能
ArrayList的定义:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
ArrayList继承自AbstractList类,实现了List接口,RandomAccess是可随机访问的标记接口,Cloneable是可克隆标记接口,Serializable是可序列化标记接口。
成员属性
// 默认ArrayList集合的大小。
private static final int DEFAULT_CAPACITY = 10;
// 如果是空集合,那么都指向这个空数组常量,减少创建多个空数组变量。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 如果是默认集合,先用这个空数组常量代替,当添加元素时,再创建DEFAULT_CAPACITY长度的数组
// 作用就是减少内存使用。
// 例如我们new 多个ArrayList集合时,只要我们还没有添加元素,它们内部elementData都指向这个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 采用Object[]数组来存储集合中的元素
transient Object[] elementData;
// 集合中元素的数量
private int size;
它有两个重要属性,elementData Object数组用来存储集合中元素,size表示集合中元素的数量。还有一个重要属性就是继承自AbstractList的modCount属性。
构造函数
默认的构造函数:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
注意这里和老版本不一样,当我们调用ArrayList空参构造时,它仅仅只是将空数组赋值给elementData,而不是创建一个DEFAULT_CAPACITY大小的新数组。当我们真正调用添加元素方法时,才会创建新数组。延迟创建默认大小的数组。
设置数组初始长度的构造函数:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
根据initialCapacity的值创建对应大小的Object数组,如果小于0就抛出异常。
注意这里没有使用延迟创建数组的方法,因为这里数组初始大小是由用户自定义的。当然也可以用一个成员变量记录,然后延迟创建,只不过它这里没有这么做。
通过Collection实例构建集合:
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray 返回的可能不是Object[]数组,所以进行处理一下
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果c是一个空集合,那么这里就设置成EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
通过toArray方法,将集合c转成数组,如果数组长度为0,那么就将 this.elementData设置成空数组常量EMPTY_ELEMENTDATA,而不是集合c转成那个空数组,就可以让这个新创建的空数组内存释放了。还要注意一点就是,c.toArray()方法返回的可能并不是Object[]数组,所以这里做了判断。
数组扩容
我们知道使用数组中存放元素的大小是固定的,而ArrayList集合好像是无限大小的,但是ArrayList集合又是通过数组来实现的,所以它一定帮我们做了数组扩容。
public void ensureCapacity(int minCapacity) {
// 如果是默认构造函数创建的ArrayList集合,那么它的最小数组长度都是DEFAULT_CAPACITY。
// 如果不是的话,那么它最小数组长度就是0
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
// 当minCapacity > minExpand,那么就有可能要对数组进行扩容。
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
这个是供给外部调用的方法,来确保数组的长度大于这个minCapacity值。
private void ensureCapacityInternal(int minCapacity) {
// 如果相等,表示通过默认构造函数创建的ArrayList集合,而且还没有创建新数组。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 取minCapacity和DEFAULT_CAPACITY中较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
这是供给类内部使用的方法,在集合的添加元素方法中都会调用这个方法,确保数组长度不小于minCapacity值。
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 表示集合修改了
// 当数组长度小于minCapacity时,就要进行扩容了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
会调用grow方法,进行数组扩容操作。
private void grow(int minCapacity) {
// 获取老数组长度
int oldCapacity = elementData.length;
// 先进行1.5倍扩容, oldCapacity >> 1 与oldCapacity/2 效果一样,但是采用右移操作,速度比除法快得多。
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果进行扩容之后,还是小于minCapacity值,那么就直接用minCapacity值当成数组长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 处理newCapacity值大于MAX_ARRAY_SIZE情况,集合容量也是有上限的。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf方法,将老数组的元素拷贝到newCapacity长度的新数组中,并返回这个新数组。
elementData = Arrays.copyOf(elementData, newCapacity);
}
进行数组扩容,先将新数组长度扩充至老数组1.5倍,如果还是小于minCapacity,那么就直接使用minCapacity作为新数组长度,然后再处理超出最大数组长度问题。最后使用Arrays.copyOf方法,将老数组元素拷贝到新数组中。
添加元素
public boolean add(E e) {
// 确保数组容量
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
在集合末尾添加元素,先确保数组容量,然后再在集合末尾添加元素,最后将集合数量size加一。
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++;
}
在集合指定索引位置添加元素。先检查索引位置是否越界,确保数组容量满足要求,然后调用System.arraycopy方法,将从索引位置开始的元素全部右移一位,再将索引位置设置成新元素element,最后集合数量size自增。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- 将集合c转成数组a,并得到数组长度numNew
- 确保集合数组长度不小于size + numNew
- 通过System.arraycopy方法,将数组a的全部元素拷贝到数组elementData的size位置之后,就是集合末尾。
- 将集合数量size加上numNew大小。
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;
}
- 检查索引位置是否越界。
- 将集合c转成数组a,并得到数组长度numNew。
- 确保集合数组长度不小于size + numNew。
- numMoved 表示原数组要移动元素的数量
- 将原数组index之后的元素(包括index),都向后移动numNew个位置
我们要向一个数组从index位置插入numNew个元素,那么这个数组从index位置开始到结尾的都要移动numNew个位置,一共移动size - index个元素。
- 将数组a中全部元素插入到elementData数组index之后位置(包括index)。
- 将集合数量size加上numNew大小。
删除元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 清理引用,让垃圾回收器可以回收无用的对象内存
elementData[--size] = null;
}
遍历数组,找到与o相等的索引位置,然后调用fastRemove方法删除这个索引位置的元素。
要删除数组index位置元素,就是将index之后的元素都向前移动一位,即使用System.arraycopy(elementData, index+1, elementData, index, numMoved)方法完成。
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;
return oldValue;
}
先通过elementData(index)方法得到index位置的元素,便于返回这个被删除的元素。然后通过System.arraycopy方法将数组index位置之后的元素都向前移动一位,最后将数组最后位置的元素置位null,并将集合容量size自减。
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
移除与集合c相同元素,和只保留与集合c相同元素都调用batchRemove方法。
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
// 如果complement为true,表示本集合只保留集合c中包含的元素
// 如果complement为false,就删除集合c中包含的元素,所以集合c中不包含的元素才保留到本集合中
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 发生异常之后的补救处理
// 如果r不等于size,表示数组没有遍历完成,将数组r位置之后的元素拷贝到数组w位置之后。
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
// w就表示现在集合的数量
w += size - r;
}
// 如果不相等,说明本集合删除元素了,就要进行一些处理
if (w != size) {
// 将w位置之后的元素置位null,方便垃圾回收器回收。
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
// 将w的值赋值给size。
size = w;
modified = true;
}
}
return modified;
}
还记得我们在AbstractCollection类中是怎么操作的么?是调用迭代器的remove方法,但是在这里就不适用,因为数组删除一个元素会导致数组的移动,很浪费时间。这里用了很巧妙的方法。
想一想我们平常怎么删除数组中特定元素的。
- 第一种是新建一个长度一样的数组,然后遍历原数组,将满足条件的元素才加入到新数组中,这样就可以删除不满足条件的元素了。这种方式效率很快,但是需要多余的内存。
- 第二种是反向遍历数组,如果发现不满足条件的元素,将这个位置之后的元素前移一位,这样就覆盖不满足条件的元素了。这种方式不需要多余内存,但是需要进行数组的移动,耗费时间。
- 第三种就采用非常巧妙的方法了,这里用到两个位置索引r和w,用r索引来遍历整个数组,用w表示保留下来元素的索引。所以遍历数组,如果满足条件,就将这个元素elementData[r]存放到elementData[w] w位置,并将w数量加一。因为r一定大于或者等于w,所以不存在前面位置的元素覆盖后面位置的元素。这种方式速度快,而且不用多余内存。
public void clear() {
modCount++;
// 清理引用,让垃圾回收器可以回收无用的对象内存
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
清除集合中元素。
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// 清理引用,让垃圾回收器可以回收无用的对象内存
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
删除集合中fromIndex位置到toIndex位置的元素(包括fromIndex,但是不包括toIndex)。通过 System.arraycopy方法将toIndex(包括toIndex位置)之后的元素全部向前移动到fromIndex位置,所以toIndex位置的元素还是保留在集合中。然后将无效位置的元素全部置位null,方便垃圾回收器回收。
替换元素
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
就是将数组对应位置替换成新元素element。
查询方法
E elementData(int index) {
return (E) elementData[index];
}
返回对应索引位置元素,并强转成E类型。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
通过elementData方法获取元素。
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
正向遍历数组,查找与o相同元素,如果查找到就返回对应下标位置,如果没有查到就返回-1。
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
反向遍历数组,查找与o相同元素,如果查找到就返回对应下标位置,如果没有查到就返回-1。
public Iterator<E> iterator() {
return new Itr();
}
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
与AbstractList一样,ArrayList提供两个迭代器,都是ArrayList内部类:Itr和ListItr。
内部类SubList
subList是List接口中定义的一个方法,该方法主要用于返回一个集合中的一段、可以理解为截取一个集合中的部分元素,它的返回值也是一个List。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!