基本介绍
ArrayList是使用数组存储元素的的集合,能够自动进行扩容。ArrayList的类图如下
该类拥有许多操作集合的方法,在这篇文章中将会debug几个常见的方法。
这里先将ArrayList的成员属性以及注释列出来
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
构造器
ArrayList的几个构造器如下
public ArrayList(int initialCapacity){
...
}
public ArrayList() {
...
}
public ArrayList(Collection<? extends E> c){
...
}
指定初始容量
在创建ArrayList的时候指定集合的大小
ArrayList<Integer> list = new ArrayList<>(20);
下面来debug一下这个流程。
首先进行到构造方法,
可以发现这个方法就是简单的判断了一下传入的容量是否大于0,如果大于0,那么初始化elementData,如果等于0,那么就将一个空数组赋值给elementData,否则就抛出异常
默认创建
现在debug一下无参构造器
ArrayList<Integer> list = new ArrayList<>();
可以看见这个构造器就是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA的引用传递给elementData,下面来看一下elementData
可以发现就是一个空数组,这个数组会在添加元素的时候初始化大小。构造器结束后发现并没有实际创建数组
通过集合创建
开始debug参数为Collection的构造器
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
Arrays.asList会返回一个ArraysList,ArrayList就是Collection的子类,下面为Arrays。asList的源代码
下面开始debug
我们来分析一下这个代码
if ((size = a.length) != 0)
上面是将a数组的长度赋值给size,并且判断是否为0
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
上面代码就判断传入的集合是否为ArrayList,如果是,那么就直接赋值,否则就创建一个和a一样大的数组.
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
最后这个就是用来出来传入集合为空的。
添加
可以发现主要有4个方法。add表示添加一个元素,addAll表示批量添加。默认是添加在数组最后一个元素后面的,也可以指定要添加的索引。对于添加到指定索引,这个应该叫修改。
add扩容机制
我们在上面的构造器中看见了如果没有指定初始容量,那么默认的初始大小就是0。我们来看看ArrayList的扩容机制是怎样的
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 11; i++) {
list.add(i);
}
}
上面代码会想集合中添加11个元素,先来看添加第一个元素的流程。首先进入add方法,最开始element为空,所以size也为0
然后进入ensureCapacityInternal方法
然后进入calculateCapacity方法,这个方法用来计算最小容量的
由于我们是通过无参构造器创建的集合,所以elementData就会等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,然后返回一个最小容量Math.max(DEFAULT_CAPACITY, minCapacity),DEFAULT_CAPACITY就是10,minCapacity等于1,所以就会返回10。然后进入ensureExplicitCapacity方法
这个方法就是用来判断集合所需的最小容量是否大于当前集合的容量,在这里,显然是大于的。然后进入grow方法
这个方法简单来说就是将当前容量扩大为1.5倍,就是下面的语句
int newCapacity = oldCapacity + (oldCapacity >> 1);
然后判断扩大后的容量是否满足最小的容量需求
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
最后就是创建新数组,容量更大(一般情况都是1.5倍大小)。然后elementData 执行新数组
elementData = Arrays.copyOf(elementData, newCapacity);
然后一直返回到add方法
此时elementData为容量10的空数组
然后进行数组赋值
这样add方法就结束了。现在,我们就可以做一个总结,add方法就是会将元素存储在最后一个元素的后面,但是存储之前会判断一下数组,也就是elementData是否还有足够大的容量。
所以,我们后面存储1-9都不会进行扩容
当我们存储10的时候,又会将容量扩大为1.5倍(对于添加单个元素),也就是15
批量添加addAll
批量添加就是addAll,传入应该Collection,然后就会将传入的Collection添加到当前集合
可以发现这个方法非常简单,就是先判断是否还有足够的容量,然后数组复制。对于
ensureCapacityInternal方法,上面说了对于单个元素基本就是扩容1.5倍了,因为扩容1.5倍后添加一个元素肯定没有问题了,但是对于批量添加的,扩容1.5倍不一定够,对于批量添加可能就是扩容为当前size+传入集合大小。
看下面例子
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 11; i++) {
list.add(i);
}
List<Integer> addList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
addList.add(i);
}
list.addAll(addList);
}
我们直接添加了100个元素,猜一下现在的list大小为多少?现在的大小为111
原因就是下面的语句
添加指定位置add
使用add(int index, E element),将指定元素插入到此列表中的指定位置。将当前位于该位置的元素(如果有的话)和所有后续元素向右移动(向它们的下标添加1)。
我们来分析一下这个方法,首先就是检查索引是否越界
然后就是移动数组元素,保存指定值
添加多个元素到指定位置addAll
使用 addAll(int index, Collection<? extends E> c),传入索引和集合
这个方法和上面基本一样都是数组复制,不细说了
删除
列表删除主要有以下几个方法,下面分别介绍
删除指定元素remove
remove(Object o),这个方法传入一个元素,从此列表中删除该元素的第一次出现。
可以发现这个方法十分简单,就是对所有元素进行比较,如果相等就调用fastRemove这个方法,将要删除元素的索引传过去
上面就是fastRemove,可以发现也是通过数组复制完成的。
删除指定索引元素remove
remove(int index),传入要删除元素的索引
上面就是该方法的源码,可以发现处理也很简单,就是先检查传入的index是否合法,然后保存要删除的值,然后数组移动,最后返回删除的值。
条件删除removeIf
removeIf(Predicate<? super E> filter) 这个方法就会根据传入的条件来进行删除,下面是该方法的源代码
@Override
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
对于上面的方法,其实思路很简单,第一步就是遍历所有元素,然后将要删除的元素索引放入一个Set。(BitSet后续文章会进行说明)
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
然后再次遍历集合,用不需要删除的元素来填充删除元素
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
最后再将集合中最后x(x表示删除元素的个数)个元素置为null
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
批量删除removeAll
removeAll(Collection<?> 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++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
这个方法我们来分解一下,首先是
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
可以发现,这个循环就理解为将数组不被删除的元素左移即可,被删除的元素将被覆盖
finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
上面代码的第一个判断没有用,直接忽略,下面一个判断 w != size,就是为了将集合中最后x(x表示删除元素的个数)个元素置为null。
修改
修改元素,我们可以使用set和replaceAll方法,下面来进行说明
修改指定位置set
使用set(int index, E element),该方法内容如下
可以发现就是检查索引是否越界,然后就将指定索引设置为新值,最后返回旧值
替换所有满足要求元素replaceAll
replaceAll(UnaryOperator operator),这个方法传入一个lambda表达式即可,会对所有满足条件的元素进行更新
该方法很简单,就是一个for循环,核心语句就是如下语句
elementData[i] = operator.apply((E) elementData[i]);
如果看不懂,那就是lambda学的不扎实,建议参考 一篇文章彻底搞懂lambda表达式
一些实用方法
上面以及介绍完了ArrayList的很多基本操作,现在给出一些ArrayList的一些实用方法及其说明
- clear方法,该方法会将集合所有元素清空
- indexOf(Object o) 返回元素第一次出现的索引
- contains(Object o) 判断集合中是否包含指定元素
- toArray(T[] a) 将集合转换为数组
- trimToSize() ,调整集合容量为当前元素个数
总结
对于ArrayList,就理解为一个简单的数组即可,该数组会自动扩容(一般为1.5)。
在ArrayList中,其实还有一些方法没有进行说明,例如forEach,sort,clone,retainAll等等,对于这些方法,原理都比较简单,大家经过上面的学习,自行查看源码即可
ArrayList删除读和取,不擅长修改和删除,如果要频繁使用修改和删除请使用LinkedList。