本文来自mrcode markdown博客:http://www.mrcode.cn/zhuqiang/article/details/20.html
关键属性:
属性 | 说明 |
---|---|
transient Object[] elementData; | 底层使用该Object数组进行存储元素,并且该属性的访问权限是:同类同包,且不序列化 |
modCount | 从java.util.AbstractList 继承的字段;记录的应该是被修改的次数,比如add一次就等于修改了一次 |
入门
用以下简单的示例进行分析
ArrayList<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("1");
list.add("6");
list.add("7");
list.add("8");
list.add("9");
list.add("10");
list.add("11");
list.remove("1");
list.remove(null);
list.remove(1);
list.contains("5");
list.ensureCapacity(5);
1.1. add 添加元素方法分析
// 步骤1:空参
public ArrayList() {
// 全局默认的空元素数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 步骤2:添加元素
public boolean add(E e) {
//负责扩容elementData仓库,当前列表长度+1.为当前数据所需要的最小容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//把元素添加到列表(对外我们看到的arraylist列表)末尾,并且让列表的大小加1
elementData[size++] = e; //由于支持的最大容量是Integer.MAX_VALUE,这里如果超过了最大容量是不是就会抛出下标越界异常呢?
return true;
}
//该方法负责扩容elementData仓库;该类add重载的方法都调用了该方法,在下图可以看到
private void ensureCapacityInternal(int minCapacity) {
// 当仓库为默认空数组的时候,计算最小容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//默认容量=10;
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// 记录修改的次数;判断是否需要进行扩容; 在jdk文档中也说了:在添加大量元素之前可以使用ensureCapacity方法来增加数组仓库的容量,减少copy的次数,提高性能
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;
// 每次扩容是源存储仓库的一半,/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 特殊判断:当this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA的时候,上面的newCapacity算出来会=0,所以这里需要初始化一个最小容量。
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:
//把旧的存储仓库进行copy到一个新的数组中,并返回
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 这里针对最大容量超过了 默认的容量的处理返回容量:MAX_ARRAY_SIZE在源码中是Integer.MAX_VALUE-8,也就是说arraylist的最大容量是Integer.MAX_VALUE,
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
所有添加元素的方法都调用了该方法进行判断扩容
1.2.remove 移除元素方法分析
// 步骤 3:移除元素
public boolean remove(Object o) {
//针对null元素的移除,和非null元素的代码差不多
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)
//从数组仓库中 index+1 到 列表size-1 之前的所有元素都重新复制到 数组仓库中:从index索引开始覆盖,最后就会存在多出来一个元素。这里也就是把删除掉元素后面的所有元素都往前进行了挪动,并且是以覆盖的方式进行挪动。
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//把多出来的元素置为null,并且把列表大小-1;
elementData[--size] = null; // clear to let GC do its work
}
1.3. contains 是否包含指定的元素方法分析
如果认真看了上面两个方法的分析,这里的就很简单了。能看出来,判断什么的几乎上都是一样的;
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// 返回此列表中首次出现的指定元素的索引
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;
}
1.4 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; //获取itr对象最后修改的次数(相当于版本吧,用来判断在itr期间该list是否被更改,用来进行快速失败,因为在迭代获取的时候,如果list被更改,那么获取到的数据不准不说,还会导致程序出错)
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification(); //检查list是否被更改,只是判断了修改版本,快速失败
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];
}
//itr 的删除
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); //同样再次检查是否修改
try {
ArrayList.this.remove(lastRet); //调用原来的删除方法删除元素,在元素删除的时候数组都会被copy一次,那么元素中的原来的索引就会发生变化
cursor = lastRet;// 重置游标到当前的元素,因为当前的被删除掉了,那么下一次获取的时候就是下一个元素的索引
lastRet = -1; //该参数是在获取的时候 根据cursor游标进行赋值的,所以这里重置为-1; 是因为如果都删除完了还调用next方法,就会抛出异常。
expectedModCount = modCount; //修正最新的修改次数。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
1.4.1. 总结
- 为什么在itr中删除元素比在fori中删除list中的元素 要方便很多,在fori中删除list元素,你要考虑到当前的索引是否超过了大小,而在itr中不用考虑呢?
因为itr中记录了当前的游标,删除操作还是委托的原来的删除方法,那么也会导致list的数组大小和元素索引发生变化,正是因为有了这个游标,才解决了上面的问题。 - 在其他方面,防止多线程对外修改导致itr内部数据计算出错,使用了检测修改次数的方式进行快速失败
总结
第一次认真的阅读Jdk的源码,网上说该工具类是相对简单的实现,先从这个类看起。现在看来果然不错。
从上面三个方法,基本上就把该类给整明白了。
- 线程是不安全的
- 底层使用一个Object数组进行存储元素
- 可自动扩容
- 允许null元素
- 由于扩容策略,在大量插入元素的时候可以使用ensureCapacity方法来增加数组容量