1 查看ArrayList源码
1.1 边界检测
边界检测就是检测索引算法越界,方法中有索引的参数都需要检测。这里以add(int index, E e)和remove(int index)为例
-
add(int index, E e):这里我们要在指定索引index位置插入元素e,那么index就不能越界。因为是插入元素所以允许的范围就是 0 ∼ s i z e 0\sim size 0∼size,查看下源码实现如下:
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
- 超出范围就报索引越界异常,是不是很熟悉😏
-
remove(int index):删除指定索引位置的元素。既然是删除元素,那么需要元素是数组中的一个,索引范围 0 ∼ s i z e − 1 0\sim size-1 0∼size−1 。查看下ArrayList源码实现:
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
-
哎为啥这里不检测索引为负的情况呢?源码给出的解释:
-
This method does *not* check if the index is * negative: It is always used immediately prior to an array access, * which throws an ArrayIndexOutOfBoundsException if index is negative.
-
大意:此方法不检测索引是负数的情况。因为,一旦索引为负数,访问数组的时候就会立即抛出数组索引越界异常。
-
1.2 扩容
在添加的元素的时候,怎么保证属性表容量是足够的呢?如果容量不够怎么解决呢?扩容策略是怎么样的呢?
那么来查看源码,源码区分首次添加元素扩容和非首次添加元素扩容:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 分析:
- 查看添加方法,ensureCapacityInternal(size + 1); 添加方法中,这里传递参数size+1指顺序表容量至少是size+1,因为要添加一个新元素,往下走
- ensureCapacityInternal 方法首先判断是否是首次添加元素,如果是,容量取默认容量和方法参数中的最大值;如果不是首次添加元素,调用 ensureExplicitCapacity(minCapacity);
- ensureExplicitCapacity方法中判断参数如果比数组长度小,那么表示容量不足,进行扩容,调用grow(minCapacity);
- grow方法首先保存旧容量oldCapacity既数组的长度,新容量为旧的容量加上就容量的一半,即扩容策略就是扩容为原来容量的 3 2 \frac{3}{2} 23;判断如果新容量小于方法参数,把方法参数赋值给新容量;再判断新容量是不是大于数组最大大小,如果大于调用hugeCapacity(minCapacity);
- hugeCapacity(minCapacity)方法先判断参数是否溢出,因为执行该方法就意味着参数大于MAX_ARRAY_SIZE,这是类中定义的常数:private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 如果溢出直接报OOM;没有在判断参数和MAX_ARRAY_SIZE的大小,前者大返回Integer.MAX_VALUE ;后者大,返回MAX_ARRAY_SIZE。
- 继续返回grow方法,保证容量的前提下,实现数组数组迁移。
- 继续返回add方法,保证容量前提下直接数组末尾插入,顺序表大小加1结束
如果后面有空画个流程图或者E-R图看起来会更直观。
1.3 foreach
对于集合操作,其中之一遍历是很常用。我们都知道要实现foreach遍历需要实现iterator接口,我们查看ArrayList源码看看,它是怎么实现的?
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
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;
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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 通过iterator方法得到一个实现了Iterator接口的内部类,看下具体实现
- 成员变量:
- cursor:光标或称下一个元素索引
- lastRet:上一个元素索引,默认-1
- 成员方法
- hasNext:cursor==size说明没有下一个元素了
- next:
- cursor赋值给i
- 检测i范围
- 如果i越界抛出相应异常;没有越界,cursor值加1,lastRet=i,返回该索引处元素
- remove:
- 判断如果lastRet小于0 抛出异常
- try块尝试调用remove(lastRet)删除元素,成功lastRet赋值cursor,lastRet值置为-1;如果产生异常调整catch快。
3 关于modCount说明
这个变量我们在ArrayList中发现很多方法中都有用到?那些方法嗯?有什么作用呢?
-
位置:用的地方呢是改变ArrayList的方法,什么算是改变,如增删改排序等等,基本上除了查询外其他方法都有用到,即方法执行的时候会将该值加1。
-
作用:这个变量用来做iterator变量时线程安全同步检测的。怎么实现的呢?就是上面说到当一个线程对该ArrayList对象做出改变的时候,modCount会加1;当另外一个线程也对同一对象做修改时,当前线程会检查该值是否改变,如果改变了就报错:ConcurrentModificationException (😏老弟又见面了),检测代码:
int expectedModCount = modCount; final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
3 改进
基本上是照搬的ArrayList源码,这里不再赘述,可以看下面仓库源代码,自行测试。
4 后记
既然有官方实现,ArrayList功能还那么强大,为什么我们还有自己实现一个类似功能的集合类型呢?
- ArrayList功能是很强大,但是不是所有的操作都是我们需要的,在某些应用场景下,使用起来很臃肿。
- 某些应用场景我们想要添加新的功能,但是又需要ArrayList中的全部方法怎么办呢?
上面这些情况都不适合用ArrayList。哎既然可以自己写,为啥还要参考ArrayList呢?ArrayList功能那么好,干嘛要自己去闭门造轮子呢?(😎自认为NB的除外)
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm
参考:
[1]百度百科.线性表[EB/OL].2022-05-08/2022-10-02.
[2]百度百科.顺序表[EB/OL].2022-05-09/2022-10-02.
[3]黑马程序员.黑马程序员Java数据结构与java算法全套教程,数据结构+算法教程全资料发布,包含154张java数据结构图[CP/OL].2020-01-18/2022-10-02.
[4]jdk ArrayList源码