欲分析ArrayList的组成,我们先来了解一下集合框架的组成。
集合框架模块主要分两个流:Collection和Map,其中
Collection:List -> ArrayList、LinkedList、Vector
Set -> HashSet、LinkedHashSet、TreeSet
Map:HashMap、LinkedHashMap、HashTable、TreeMap
ArrayList就是实现了List接口。下面看看Collection和List的区别:
public interface Collection<E> extends Iterable<E> {
int size();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
}
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
}
List是继承Collection的,故List具有collection所有的方法,List比Collection多了一些索引和获取的方法。
在被ArrayList继承AbstractList抽象类中有一个实现Iterable(还记得迭代器的遍历吗?)的私有类Itr:
private class Itr implements Iterator<E> {
int cursor = 0; // 记录即将调用索引的位置
int lastRet = -1; // 最后一个元素的索引
int expectedModCount = modCount; // 线程安全标记、并发标记
// 判断是否有下一个元素
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
modCount:记录所有数组数据结构变动的次数(增删改),为了避免并发操作多个线程同时对一个数据进行操作,当某个线程修改了数组结构,而另一个线程恰恰读取这个数组时就会产生错误。所以modCount != expectedModCount,比如线程A对数据结构修改一次,那么modCount比如+1,而expectedModCount不变就会产生异常。
这里remove删除只是将索引值删除并将expectedModCount重新赋值,并没有也并不能回收内存。
private static final long serialVersionUID = 8683452581122892189L;// 序列化id
private static final int DEFAULT_CAPACITY = 10;// 默认初始的容量
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];// 一个空对象
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;// 当前数据对象存放地方,当前对象不参与序列化
private int size;// 当前数组长度
private static final int MAX_ARRAY_SIZE = 2147483639;// 数组最大长度
这里需要注意的是默认数据对象的容量是10。
在分析属性时发现一个比较有意思的地方,那就是ArrayList数据对象存elementData是transient修饰的。要理解为什么这么做,那我们先了解一下序列化实现类和transient。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它,关键字transient就是这个作用。
既然理解了transient可以用来干什么,那么我们也就可以推断出为什么用transient修饰element:elementData是一个缓存数组,一般会预留一些容量,当容量不够时再进行扩充,所以通常会存有空元素和多余的空间,就没有必要进行存储序列化。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out array length
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
虽然多余的空间没有必要序列化,但是实际存在的元素还是进行了序列化,实现在writeObject方法中,将索引到的元素以此写入文件中。
然后是三种构造器,没什么可说的。然后trimToSize方法可类比String的trim方法
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
将无用内存引用去除掉,减少内存开销,可用于性能优化。
接下来是扩容方法,扩容计算:newCapacity = oldCapacity + (oldCapacity >> 1); 扩充当前容量的1.5倍
public void ensureCapacity(int minCapacity) {
// 最小扩充容量,默认是 10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
// 若用户指定的最小容量 > 最小扩充容量,则以用户指定的为准,否则还是 10
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity= Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 判断指定扩充量是否大于最小扩充容量,若不是则不必扩容。
- modCount自加,判断最小扩充容量是否大于当前数组长度。
- 若大于,则(听说是叫扩容因子)newCapacity = oldCapacity + (oldCapacity >> 1); 扩容后的长度newCapacity它的值为原数组长度+该长度向左移1个长度(即除以2)。
- hugeCapacity是大容量扩容,扩至MAX_ARRAY_SIZE。
然后就是一些索引方法和一些基本方法,没什么可说的,值得注意的是下面的clone方法:
public Object clone() {
try {
// Object 的克隆方法:会复制本对象及其内所有基本类型成员和 String 类型成员,但不会复制对象成员、引用对象
ArrayList<?> v = (ArrayList<?>) super.clone();
// 对需要进行复制的引用变量,进行独立的拷贝:将存储的元素移入新的 ArrayList 中
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
这是一个深度复制的方法,至于clone的深复制和浅复制的区别可以自己去了解一下。
那接下来我们来分析一下modCount的作用:
它在AbstractList里定义:protected transient int modCount = 0;受保护、不可被序列化。是不是有点像锁。。。
在看对modCount进行了操作的方法:
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
modCount++,实现fail-fast机制:在集合上对结构进行改变而产生的一种错误检测机制
扩容:
private void ensureExplicitCapacity(int minCapacity) {
// 将“修改统计数”+1,该变量主要是用来实现fail-fast机制的
modCount++;
// 防止溢出代码:确保指定的最小容量 > 数组缓冲区当前的长度
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
复制:
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
移除:
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;
}
private void fastRemove(int index) {
modCount++;//这个地方改变了modCount的值了
int numMoved = size - index - 1;//移动的个数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //将最后一个元素清除
}
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
由此我们可以发现每次增删都有modCount++,改查就不变。因为该集合是一个数组,如果进行增加或删除,则size必然改变,那么在遍历过程中,肯定找不到正确的结果或者直接数组越界异常,这都是我们不想要的,这里的modCount改变就映证了开头所解释的迭代器中modCount != expectedModCount,也就是说modCount的设计是为了检测并发处理数据改变集合结构而导致遍历集合时数组越界产生异常的情况。
- fail-fast 快速失败,产生于遍历集合过程中,如某个线程对集合集合遍历时,有另外线程对该集合做修改操作,则会抛出ConcurrentModificationException异常,其作用是用来检测错误,并不一定发生。
- fail-safe 安全失败,在遍历集合,并不是直接在原集合中操作,而是先复制一个集合,在复制的集合中操作,这样就不会产生ConcurrentModificationException异常。缺点是改变后不能看到改变后的结果。
其中安全失败里提到的复制集合,并不是直接将原来的集合赋给新集合,如list_1=list_2,这样只是把内存中的引用地址给了list_1,如果list_1也进行增删操作,那么list_2也能得到相应的结果,即两个集合操作都是对同一个堆内存的地址里的数据进行操作,前面说的复制是另开辟一个堆内存。(切记)。
在了解这两个概念后,应该可以猜的出来,modCount的作用就是用来fail-fast的。
至于线程安全性的问题 很明显ArrayList是线程不安全的。
超详细解析:https://www.cnblogs.com/gxl1995/p/7534171344218b3784f1beb90d621337.html