1.前言
集合对于开发者来说都不陌生,可以说是我们日常开发中使用最频繁的对象之一,尤其是ArrayList,可是对于一些开发者并不真正了解它,只是使用习惯了,也就按照集合中基础的一些api使用了,但有时候却因为错误的使用集合导致代码的性能较差,甚至出现致命错误的代码。
前几天在做代码review的时候,发现有同事提交了这么一段代码,它的意图就是从文章列表中删除标题不合法的的文章。
下面我简单给大家看一下(这里去掉了一些附属的代码,只做基本代码的说明):
List<Article> articleList = new ArrayList<>();
Article article = new Article("xxx");
articleList.add(article);
articleList.add(article);
articleList.add(article);
articleList.add(article);
String removeTitle = "xxx";
for (Article a : articleList) {
if (removeTitle.equals(a.getTitle())) {
articleList.remove(a);
}
}
这位同事还不是很服气,觉得这么写没多大问题,之前很多代码就是这么写的啊。基于此,我们从头分析一下。
2.ArrayList
2.1 ArrayList 类的层次结构
ArrayList实现了List、RandomAccess、Cloneable、Serializable接口,继承了AbstractList抽象类。通过实现RandomAccess接口,可以实现集合的随机访问;通过实现Cloneable、Serializable接口,可以实现克隆和序列化。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2.2 ArrayList 属性及底层实现
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}
ArrayList主要有size(数组长度)、elementData(底层对象数组)、DEFAULT_CAPACITY(初始容量,默认10)、EMPTY_ELEMENTDATA(底层共享的空数组实例)。基于此,数组底层其实就是基于数组来实现的,并且使用数组来实现动态扩容。
如果我们仔细看它的源码 ,会发现比较奇怪的地方,就是elementData属性加上了transient修饰(禁止序列化),可是ArrayList明明实现了Serializable接口啊。这是因为ArrayList的数组是基于动态扩容,并不是所有被分配的数组空间 都存在元素,所以如果采用外部的序列化方法,就会序列化整个数组,这就导致这些没有存储数据的内存空间也会被序列化;相反,ArrayList内部提供了两个私有方法writeObject以及readObject来自我完成序列化和反序列化,从而节省内存空间。
2.3 ArrayList的构造函数
ArrayList一共有三个构造函数:
1.List list= new ArrayList<>();默认构造函数,创建一个空数组对象:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.List list= new ArrayList<>(20);传入一个初始容量值的构造函数:
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);
}
}
3.传入一个集合类型进行初始化:
HashSet<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set.add("a");
List<String> list= new ArrayList<>(set);
源码如下:
//传入一个集合类型进行初始化。
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;
}
}
2.4 ArrayList的基本方法
2.4.1 ArrayList获取元素 list.get(i)
由于ArrayList是底层是基于数组实现的, 实现了随机访问接口,所以在获取元素的时候是非常快的。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
2.4.2 ArrayList新增元素
ArrayList有两种新增元素的方法:
1.add(E e):直接将元素加入到数组的末尾;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.add(int index, E element);添加元素到任意位置(通过指定下标)
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++;
}
从源码可以看到,这两个方法在添加元素之前都会检查确认容量大小,如果容量不够大,就会按照原来数组的1.5倍进行动态扩容,扩容之后将数组复制到新的数组中。同时我们我们还可以看出,添加元素到任意位置,会导致该位置后面的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序的过程。所以我们在初始化时如果知道了存储数据的个数,可以指定数组的容量大小,这样可以避免数据的动态扩容;同时,添加元素的时候从末尾添加,避免元素的重排。我们可以考虑 从以上这两个方法来提高性能。
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++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
}
2.4.3 ArrayList删除元素 remove(Object o)
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
}
从源码中可以看到,ArrayList删除元素与添加元素到任意位置的方法有相同之处,ArrayList每次删除元素后,都要进行数组的重排(除非从尾部删除),删除的元素的下标越小,数组重排的开销就越大。
2.4.4 ArrayList 遍历
2.4.4.1 使用下标索引遍历 for(; ; )
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
2.4.4.2 使用foreach遍历 for(😃
for (String s : list) {
System.out.println(s);
}
2.4.4.3 使用迭代器遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
但其实使用foreach遍历和使用迭代器遍历是一样的,使用foreach遍历,代码编译的时候也会转变成迭代器遍历:
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String s = (String)iterator.next();
System.out.println(s);
}
3. 错误分析及解决
最初那段代码执行报错:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.zyxds.Article.main(Article.java:38)
那么为什么呢?从我们上面的对ArrayList的分析来看,这段代码最终会被编译器优化成如下:
List<Article> articleList = new ArrayList();
Article article = new Article("xxx");
articleList.add(article);
articleList.add(article);
articleList.add(article);
articleList.add(article);
String removeTitle = "xxx";
Iterator var7 = articleList.iterator();
while(var7.hasNext()) {
Article a = (Article)var7.next();
if (removeTitle.equals(a.getTitle())) {
articleList.remove(a);
}
}
}
即foreach被优化成了迭代器.而迭代器中的next()方法,会检查modCount与expectedModCount是否相等:
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
但是我们看删除方法articleList.remove(a);它调用了articleList的删除方法,然后通过fastRemove()方法进行删除:
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
}
在fastRemove()方法中,仅仅改变了modCount的值,而并没有体现expectedModCount的变化,因为expectedModCount是属于Itr,即Iterator迭代器的属性:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
}
那应该怎么正确删除呢?首先使用迭代器遍历,然后调用迭代器的删除方法就可以了。
List<Article> articleList = new ArrayList<>();
Article article = new Article("xxx");
articleList.add(article);
articleList.add(article);
articleList.add(article);
articleList.add(article);
String removeTitle = "xxx";
Iterator<Article> itr = articleList.iterator();
while (itr.hasNext()) {
Article nextArticle = itr.next();
if (removeTitle.equals(nextArticle.getTitle())) {
itr.remove();
}
}
我们这里顺便看下迭代器的删除方法的源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//会设置expectedModCount,使其等于modCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}