一、ArrayList源码分析
1.1 成员变量
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//对象数组,存储具体的元素
transient Object[] elementData;
//元素的个数
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
1.2 构造方法
/**
* 带初始化容量的构造方法
*/
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);
}
}
/**
* 无参构造方法
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 使用指定 Collection 来构造 ArrayList
*/
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
1.3 add操作
public boolean add(E e) {
//确保容量是否够用,不够用的话就扩容
ensureCapacityInternal(size + 1);
//新元素进入数组
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//calculateCapacity(elementData, minCapacity)等于10或size+1
}
// 计算容量,如果数组为空则取10和size+1中的较大者,否则就是size+1
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);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//新容量=旧容量+旧容量的一半,也就是原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量溢出了,那么新容量就是传进来的参数,也就是10或者是size+1
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新容量比规定的最大的数都大,那么就是最大的数
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//将旧数组的元素copy到新的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
add流程:
- 先计算所需要的容量,如果不够就进入2,否则进入3。
- 扩容,扩容后的容量是之前的1.5倍,将旧数组的元素copy到新的数组,进入3。
- 将新的元素加入到数组的末尾。
- 返回true。
1.4 get操作
public E get(int index) {
//检查index是否超过size,超过则抛出异常
rangeCheck(index);
return elementData(index);
}
1.5 remove操作
//根据数组下标删除元素
public E remove(int index) {
//检查index是否超过size,超过则抛出异常
rangeCheck(index);
//修改的次数加1
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;
}
//根据具体元素删除元素
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; // clear to let GC do its work
}
根据下标删除元素的流程:
- 先判断下标是否小于size。
- 如果要删除的元素不是最后一个元素,调用System.arraycopy()方法,将要删除元素后面所有的元素复制到前一位上。
- 将最后一个元素置为空。
根据元素删除元素的流程:
- 如果要删除的元素为null,遍历数组,用等于号来判断元素是否相等,如果相等则按照上面的方式删除。
- 如果要删除的元素不为null,遍历数组,用equals方法来判断元素是否相等,如果相等则按照上面的方式删除。
1.6 缩容操作
//如果size小于length则可以进行缩容操作
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
二、ArrayList常见面试问题
2.1 ArrayList的默认初始长度是多少?
ArrayList的默认初始长度是10。
2.2 ArrayList如何扩容?
一般扩容后的容量是原来的1.5倍,如果超过了整型的最大值,那么就是Integer.MAX_VALUE。容量确定后底层调用System.arraycopy()方法将旧的数组复制到新数组中。
2.3 遍历删除问题
删除ArrayList中所有"b"元素:
public static void main(String[] args) {
//ArrayList 中有{"a","b","b","c","d","b","e","e","e",}
//删除所有"b"
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
list.add("b");
list.add("e");
list.add("e");
list.add("e");
System.out.println("删除操作之前:"+list);
for(int i = 0; i < list.size(); i++) {
if ("b".equals(list.get(i))){
list.remove(i);
}
}
System.out.println("删除操作之后:"+list);
}
public static void main(String[] args) {
//ArrayList 中有{"a","b","b","c","d","b","e","e","e",}
//删除所有"b"
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
list.add("b");
list.add("e");
list.add("e");
list.add("e");
System.out.println("删除操作之前:"+list);
for (String str : list) {
if (str.equals("b")) {
list.remove(str);
}
}
System.out.println("删除操作之后:"+list);
}
结果:
删除操作之前:[a, b, b, c, d, b, e, e, e]
删除操作之后:[a, b, c, d, e, e, e]
删除操作之前:[a, b, b, c, d, b, e, e, e]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.example.demo.thread.ArrayListTest.main(ArrayListTest.java:22)
现象:普通for循环元素b没有删除干净,foreach循环报错。
原因:remove()方法会使元素的位置向前挪动一位,如果存在相同且相邻的元素,后面相同的元素会代替它前面一位已经删除的元素,此时继续遍历的话就会判断下一位元素。
解决方法:
- 普通循环倒叙遍历删除。因为后面元素的位移不影响前面的元素。
for (int i = list.size() - 1; i > 0; i--) {
if ("b".equals(list.get(i))) {
list.remove(i);
}
}
- 用迭代器解决。
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("b")) {
iterator.remove();
}
}
- removeIf()方法解决。
list.removeIf(a -> a.equals("b"));
2.4 ArrayList线程安全吗?有线程安全的list吗?
ArrayList线程不安全,CopyOnWriteArrayList是线程安全的list。
三:补充
ArrayList里的迭代器
思考:为什么在2.3中使用foreach会抛出异常呢?
源码:
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的下标
int lastRet = -1; // 上一个元素的下标,如果没有的话是-1
int expectedModCount = modCount; // 对 ArrayList 修改次数的期望值
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//省略
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
-
hasNext
如果下一个元素的下标达到了数组的size,说明到最后了。
-
next
先判断expectedModCount和modCount是否相等,注意expectedModCount是Itr类独有的成员变量而
modCount是整个ArrayList的成员变量,如果不相等就直接抛异常,也就是说通过迭代器的修改次数和通过ArrayList的修改次数不一致就会抛出异常;如果相等就继续判断cursor是否超出了size和length,超出则抛异常,没有超出则将cursor赋值给lastRet并取出相应的元素。 -
remove
先判断lastRet是否小于0,然后再检查expectedModCount和modCount是否相等,如果相等则调用
ArrayList的remove()方法删除下标为lastRet的元素,然后将lastRet赋值给cursor,lastRet重新设置为-1,并将modCount重新赋值给expectedModCount,因为调用ArrayList的remove()方法会修改modCount。
现在说一下2.3中使用foreach这种循环方法删除ArrayList中的元素会抛出ConcurrentModificationException这个异常的原因:
首先foreach这种语法糖底层是iterator,也就是说使用的迭代器进行遍历,但是在迭代器的遍历中使用的却是ArrayList的remove()方法,也就是说modCount更新了但是迭代器里的expectedModCount没有更新,所以在迭代器调用next()方法进行遍历时,首先调用的checkForComodification()这个方法判断expectedModCount和modCount是否相等,发现不相等就抛出了ConcurrentModificationException这个异常。解决的方法就是使用迭代器遍历,并且调用迭代器的remove()方法,这个方法会将modCount同步给expectedModCount。
四:总结
- ArrayList是动态扩容的数组,按照元素的进入顺序,元素可重复,增删慢,查询快,初始长度是10,扩容后一般是原来的1.5倍,线程不安全。
- 最好对ArrayList指定初始大小,这样可以减少频繁扩容带来的开销。
- 最好使用迭代器来进行遍历删除的操作。