从源码角度彻底搞懂ArrayList

 

天天使用的ArrayList,今天咱们从深层次的源码角度来分析分析。...



今日科技快讯
近日,富士康工业互联网股份有限公司的招股书在证监会网站预披露,拟在上交所上市。国内科技类最大公司是海康威视3400多亿,360借壳成功也是3400多亿,富士康若是能上市成功,很可能成为A股市值最高的科技股龙头。
作者简介
大家周一早上好!新的一周又开始了,继续整理好新的心情加油吧!

本篇来自 潇风寒月 的投稿,分享了从源码的角度来分析了ArrayList,一起来看看!希望大家喜欢。

潇风寒月 的博客地址:

https://blog.csdn.net/xfhy_

前言


学Java很久了,一直处于使用API+查API的状态,不了解原理,久而久之总是觉得很虚,作为一名合格的程序员这是不允许的,不能一直当API Player,我们要去了解分析底层实现,下次在使用时才能知己知彼.知道在什么时候该用什么方法和什么类比较合适.
ArrayList基本特点
  • 快速随机访问
  • 允许存放多个null元素
  • 底层是Object数组
  • 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),改对应索引元素比较快
ArrayList的继承关系




来看下源码中的定义

public class ArrayList extends AbstractList

implements List, RandomAccess, Cloneable, java.io.Serializable


  • 可以看到继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作.对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类.
  • 实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合.
  • 实现了RandomAccess接口标识着其支持随机快速访问,实际上,我们查看RandomAccess源码可以看到,其实里面什么都没有定义.因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1).
  • 实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制(不知道此概念的赶快去查资料,这知识点非常重要).
  • 实现了Serializable 标识着集合可被序列化。
ArrayList 的构造方法
在说构造方法之前我们要先看下与构造参数有关的几个全局变量:

/**

* ArrayList 默认的数组容量

*/

private static final int DEFAULT_CAPACITY = 10;

/**

* 用于空实例的共享空数组实例

*/

private static final Object[] EMPTY_ELEMENTDATA = {};

/**

* 另一个共享空数组实例,用的不多,用于区别上面的EMPTY_ELEMENTDATA

*/

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**

* ArrayList底层的容器

*/
transient Object[] elementData; // non-private to simplify nested class access

//当前存放了多少个元素   并非数组大小
private int size;


注意到,底层容器数组的前面有一个transient关键字,啥意思??

查阅资料后,大概知道:transient标识之后是不被序列化的

但是ArrayList实际容器就是这个数组为什么标记为不序列化??那岂不是反序列化时会丢失原来的数据?
其实是ArrayList在序列化的时候会调用writeObject(),直接将size和element写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。

原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

无参构造方法

/**

* 构造一个初始容量为10的空列表。

*/
public ArrayList() {

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}


命名里面讲elementData指向了一个空数组,为什么注释却说初始容量为10。这里先卖个关子,稍后分析。

指定初始容量的构造方法

public ArrayList(int initialCapacity) {

//容量>0 -> 构建数组

if (initialCapacity > 0) {

this.elementData = new Object[initialCapacity];

} else if (initialCapacity == 0) {

//容量==0  指向空数组

this.elementData = EMPTY_ELEMENTDATA;

} else {

//容量 c) {

//判空

Objects.requireNonNull(c);

return batchRemove(c, false);
}

//complement是true 则移除elementData中除了c以外的其他元素
//complement是false 则移除c和elementData(当前列表的数组)都含有的元素
private boolean batchRemove(Collection c, boolean complement) {

//1. 引用不可变

final Object[] elementData = this.elementData;

//r 是记录整个数组下标的, w是记录有效元素索引的

int r = 0, w = 0;

boolean modified = false;

try {

//2. 循环遍历数组

for (; r < size; r++)

//3. 如果complement为false  相当于是取c在elementData中的补集,c包含则不记录,c不包含则记录

//如果complement为true  相当于是取c和elementData的交集,c包含则记录,c不包含则不记录

if (c.contains(elementData[r]) == complement)

elementData[w++] = elementData[r];   //r是正在遍历的位置  w是用于记录有效元素的   在w之前的全是有效元素,w之后的会被"删除"

} finally {

// Preserve behavioral compatibility with AbstractCollection,

// even if c.contains() throws.

//4. 如果上面在遍历的过程中出错了,那么r肯定不等于size,于是源码就将出错位置r后面的元素全部放到w后面

if (r != size) {

System.arraycopy(elementData, r,

elementData, w,

size - r);

w += size - r;

}

//5. 如果w是不等于size,那么说明是需要删除元素的    否则不需要删除任何元素

if (w != size) {

// clear to let GC do its work

//6. 将w之后的元素全部置空  因为这些已经没用了,置空方便GC回收

for (int i = w; i < size; i++)

elementData = null;

modCount += size - w;

//7. 记录当前有效元素

size = w;

//8. 标记已修改

modified = true;

}

}

return modified;
}


大体思路:

  1. 首先我们进行c集合检查,判断是否为null
  2. 然后我们调用batchRemove()方法去移除 c集合与当前列表的交集
  3. 循环遍历当前数组,记录c集合中没有的元素,放在前面(记录下标为w),w前面的是留下来的元素,w后面的是需要删除的数据
  4. 第3步可能会出错,出错的情况下,则将出错位置的后面的全部保留下来,不删除
  5. 然后就是将w之后的元素全部置空(方便GC回收),然后将size(标记当前数组有效元素)的值赋值为w,即完成了删除工作
再笼统一点说吧,其实就是将当前数组(elementData)中未包含在c中的元素,全部放在elementData数组的最前面,假设为w个,最后再统一置空后面的元素,并且记录当前数组有效元素个数为w.即完成了删除工作.

  • 清空列表
clear() 方法作用:清空当前集合的所有元素

这个方法非常简单,就是将数组所有元素都置为null,然后GC就有机会去把它回收了

public void clear() {

modCount++;

// clear to let GC do its work

for (int i = 0; i < size; i++)

elementData = null;

size = 0;
}


  • 移除相应区间内的所有元素(protected)

removeRange(int fromIndex, int toIndex)方法作用:移除指定区间内的所有元素,注意这是protected方法,既然是移除元素,那么就拿出来欣赏欣赏.

//这是protected方法    移除相应区间内的所有元素
protected void removeRange(int fromIndex, int toIndex) {

modCount++;

//1. toIndex后面的元素需要保留下来,记录一下toIndex后面的元素个数

int numMoved = size - toIndex;

//2. 将toIndex后面的元素复制到fromIndex处

System.arraycopy(elementData, toIndex, elementData, fromIndex,

numMoved);

// clear to let GC do its work

//3. 将有效元素后面的元素置空

int newSize = size - (toIndex-fromIndex);

for (int i = newSize; i < size; i++) {

elementData = null;

}

//4. 记录当前有效元素个数为size - (toIndex-fromIndex)  ,即减去那个区间内的元素个数

size = newSize;
}


大体思路:

  1. 假设需要移除(fromIndex,toIndex)区间内的元素,那么将toIndex后面的元素复制到fromIndex处
  2. 将有效元素后面的元素置空

改动元素

  • 替换指定下标的元素内容

set(int index, E element):替换index索引处的元素为element,可能会抛出IndexOutOfBoundsException.这里比较简单,就是将index处的元素替换成element

public E set(int index, E element) {

//1. 入参检测

rangeCheck(index);

//2. 记录原来该index处的值

E oldValue = elementData(index);

//3. 替换

elementData[index] = element;

return oldValue;
}

查询元素

  • 返回指定位置处元素

这个非常简单,就是将index索引处的数组的值返回

E elementData(int index) {

return (E) elementData[index];
}

/**
* 返回指定位置处元素
*/
public E get(int index) {

rangeCheck(index);

return elementData(index);
}


  • 通过iterator()遍历

这也是查询的一种,哈哈

首先我们了解一下fail-fast,fail-fast 机制是java集合(Collection)中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。

我们先来看看iterator()方法,它new了一个Itr(ArrayList的内部类)进行返回.

/**
* Returns an iterator over the elements in this list in proper sequence.
* The returned iterator is fail-fast.
* @return an iterator over the elements in this list in proper sequence

以适当的顺序返回此列表中元素的迭代器。  fail-fast:快速失败?
*/
public Iterator iterator() {

return new Itr();
}


接下来我们来看看这个内部类

[code]private class Itr implements Iterator {

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() {

//就是比一下cursor与size的大小   但是为什么是!=,而不是cursor= size)

throw new NoSuchElementException();

Object[] elementData = ArrayList.this.elementData;

//不能访问超出elementData.length的索引    可能是被其他线程改动了

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


    关注 郭霖


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册