您的位置:首页 > 数据库 > Oracle

ArrayList 简单的源码解析(Oracle JDK 1.8.0_211)

2020-01-13 09:51 232 查看

ArrayList 简单的源码解析

序言:为什么是简单的,因为,我看了源码,知道了一些ArrayList的怎么实现数据的增删改查的,然后算是一个记录,同时也是分享.说不定有什么理解错误的地方,请大家一定要不吝赐教,帮我指正.本文所讲述的代码是Oracle JDK 1.8.0_211版本,不同的版本可能会有差别,我不在做版本的对比.

1.先说ArrayList这个类:


继承自AbstractList,实现了四个接口List ,RandomAccess:支持随机访问,Cloneable:可以克隆,Serializable:序列化.除了List 其他三个接口,都是空的,没有我们需要重写的函数.
然后是初始化方式

1.1 几个重要的变量

  • Object[] elementData : 用来存储元素的数组
    我觉得有必要给大家看一下源码中的注释,因为这很好的介绍了ArrayLsit的几个特点
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
* 1.存储ArrayList元素的数组缓冲区。
*2.ArrayList的容量是此数组缓冲区的长度。 任何
*使用elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA清空ArrayList
*3.添加第一个元素时,将扩展为DEFAULT_CAPACITY。
*/
transient Object[] elementData;

介绍一共分三条,
第一条:介绍这个变量的含义
第二条:介绍ArrayList的一个清空方法,
第三条:介绍了ArrayList的一个属性特点,又或者叫特性

  • Object[] EMPTY_ELEMENTDATA : 用于空实例的共享空数组实例。
    这个介绍有点难以理解,简单点,就是一个默认的空数据
  • Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA :
    用于默认大小的空实例的共享空数组实例。 我们
    将此与EMPTY_ELEMENTDATA区分开以了解何时膨胀多少
    第一个元素被添加。
  • int DEFAULT_CAPACITY = 10 : 默认初始容量
    注意:并不是ArrayList初始化的时候,elementData的capacity并不是DEFAULT_CAPACITY,而是0
  • int size;ArrayList的size,就是我们平时getSize()获取到的值
    注意:这个size并不等于elementData.length
  • private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    ArrayList中数组elementData的最大容量

2.构造函数

三个构造函数,话不多说,先上图:

差别在于参数,
1.我们先看无参构造

public ArrayList() {
//给变量赋值.
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

就这么一句话,给变量elementData赋值.一个空的数组,看到没,并没有用DEFAULT_CAPACITY,所以elementData.length此时是0.
2.int 类型的构造函数

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//如果大于0
//直接将传递进来的参数当做数组的默认长度
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//等于0
//用EMPTY_ELEMENTDATA初始化数组.此时数组长度等于0,注意,用的是EMPTY_ELEMENTDATA
//并不是无参构造中的空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//其他情况,直接抛异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

3.Collection类型参数的构造函数:

public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();//直接将参数赋值给elementData
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//c.toArray可能(错误地)不返回Object [](参见6260652)
if (elementData.getClass() != Object[].class) //防错操作
//将传递过来的c,copy给elementData
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

3.增删改查

1.增

一共有四个add方法,这个大家应该都不陌生吧

  1. add(E e)
  2. add(int index, E element)
  3. addAll(Collection<? extends E> c)
  4. addAll(int index, Collection<? extends E> c)
先说第一个add(E e)
public boolean add(E e) {
//将数组扩容
ensureCapacityInternal(size + 1);  // Increments modCount!!,数组的增量
elementData[size++] = e;//将元素添加到size++,注意,不是数组的最后一位
return true;
}

//我们看到,第一步是先将数组扩容的方法,扩容完成后,第二步直接将数据元素添加到数组的size++的位置
//第二步没啥问题,非常好理解,那第一步是怎么扩容的呢?
我们进入方法中看看到底ArrayList是怎么扩容的,默认长度10是怎么设置的?

private void ensureCapacityInternal(int minCapacity) {
//调用两个方法,
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

先去看看calculateCapacity这个方法干啥了

private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果数组等于默认的空,注意了,默认的空,就是无参构造初始化的ArrayList时,设置的空数组
//如果是有参构造,就不是这个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
/**取DEFAULT_CAPACITY  ,minCapacity 中较大的值,看到了没
*默认初始容量在这里被用到,也就是在无参构造中,注释的第三条,
* 初次添加元素时,扩容到10.
* 返回值,DEFAULT_CAPACITY, minCapacity中较大的那个
*
*/
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//返回最小容量
return minCapacity;
}

然后我们在看看,ensureExplicitCapacity方法中,怎么使用的我们计算过的数组容量.

private void ensureExplicitCapacity(int minCapacity) {
modCount++;//+1

// overflow-conscious code
/**
* 判断,如果,我们需要的最小容量,大于elementData,就进行扩容
* 注意,这里的elementData.length 是数组的长度,不是ArrayList的size
*/
if (minCapacity - elementData.length > 0)
grow(minCapacity);//扩容的方法
}

我们再去看看,到底数组elementData是怎么进行扩容的,扩大的容量又是怎么计算的

/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以确保它至少能容纳
* 最小容量参数指定的元素数。
* @param minCapacity the desired minimum capacity
* 参数minCapacity 所需的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//旧的容量,用oldCapacity记录
/**
* 新的容量 = 旧的容量 + 旧的的容量右移一位
* (右移一位,代表除以二 int型 比如:3 >> 1 = 1 ,4>> 1 = 2)
*/
int newCapacity = oldCapacity + (oldCapacity >> 1);
/**
* newCapacity 最小值判断,
* 如果计算过后的newCapacity 小于,我们所需要的最小容量minCapacity
* 就使用minCapacity 作为 newCapacity
*/
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

/**
*
* newCapacity 最大值判断
* 如果我们所需要的newCapacity,大于了最大值
*/
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);
}

这个hugeCapacity方法怎么计算的呢?来瞧一瞧看一看啦!

private static int hugeCapacity(int minCapacity) {
/**
*
* 如果出现小于0的情况,直接抛出异常为什么是OutOfMemoryError
* 通过我们上边几步的判断,可以推测出,如果小于0,很有可能是数据太大,超过了Int的范围,导致表示越界,出现异常数据
* 数据量太大,所以抛出异常OutOfMemoryError
*/
if (minCapacity < 0) // overflow.
throw new OutOfMemoryError();
/**

* 如果我们需要的最小容量,大于MAX_ARRAY_SIZE
*  取 Integer.MAX_VALUE
*  否则取MAX_ARRAY_SIZE
* 所以,我们可以从这段代码看出,ArrayList的最大值,并不是MAX_ARRAY_SIZE
* 而是Integer.MAX_VALUE
*/
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

计算完成后,走到最后一步,扩容并复制数据到新的数组.我们来到Arrays类中来看看,copyOf方法

@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

直接调用了另外一个方法

/**
* <U> : 原始数组中对象的类
* <T>:返回数组中对象的类
* original:要复制的数组
* newLength:要返回的副本的长度
* newType:要返回的副本的类
* 返回值:原始数组的副本,用空值截断或填充以获得指定的长度
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class) //如果类型相同,都是Object.getclass
? (T[]) new Object[newLength]//创建一个新的数组
: (T[]) Array.newInstance(newType.getComponentType(), newLength);//否则,抛出异常
/**
*
* 将数组的里的数据拷贝到新的数组中,native方法

*/
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

/**
* src      	原数组
* srcPos 	原数组中的起始位置
* dest		目标数组
* destPos	目标数组中的起始位置
* length   	要复制的数组元素的数量。
*/
//System.java
public static native void arraycopy(Object src,  int  srcPos,
Object dest, int destPos,
int length);

第一个添加方法到此结束,我们做个简单的总结:

第二个add(int index, E element)
/**
* 将指定元素插入此列表中的指定位置。 将当前位置的元素(如果有)和任何后续元素向右移动(将其添加到其索引中)。
*
*
*/
public void add(int index, E element) {
//检查index是否可用
rangeCheckForAdd(index);
//对数组进行扩容操作
ensureCapacityInternal(size + 1);  // Increments modCount!!
//拷贝数据到新的数组
/**
* 此时已经对elementData扩容完成,但是数据还是旧的数据,
* 将elementData从index开始,一直到elementData[size]的数据,向后移动一位
* 移动后,index就是空
*/
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//index设置元素值
elementData[index] = element;
size++;//
}
第三个addAll(Collection<? extends E> c)

直接添加一个<? extends E>的ArrayList
添加的位置是在size后边,基本的步骤也跟add一样,不过传递过来的参数是Collection,所以要计算的值变成了 c.length,而不是1.注意:是c.length

public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();//将c 转换成数组
int numNew = a.length;//数组的长度作为要扩容的量
ensureCapacityInternal(size + numNew);  // Increments modCount扩容方法,跟add一样
//将a 从0开始,到numNew 的数据拷贝到elementData从size开的
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;//更新size
return numNew != 0;//返回布尔类型的返回值
}

更前两个add()方法不同的是,先将c,通过toArray转换成了数组Object[] a,怎么转换的呢?这个toArray方法是
Collection接口中的方法,它的实现在抽象类AbstractCollection中,ArrayList 继承自AbstractList ,而AbstractList就继承自AbstractCollection.
我们去看看toArray具体是怎么做的

public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
//新建数组,长度为ArrayList的size
Object[] r = new Object[size()];
Iterator<E> it = iterator();//
for (int i = 0; i < r.length; i++) {//
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
r[i] = it.next();//将r[i]设置元素为it.next()
}
return it.hasNext() ? finishToArray(r, it) : r;//返回组装好的新的数组
}

接下来就是第四个添加

addAll(int index, Collection<? extends E> c)

话不多说,直接上代码

public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);//检查index合法性

Object[] a = c.toArray();//转换c为数组
int numNew = a.length;
ensureCapacityInternal(size + numNew);  // Increments modCount扩容

int numMoved = size - index;//向后移动的数量
if (numMoved > 0)//如果需要移动
//将elementData从index开始直到size的数据,像后移动numNew个位置
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//拷贝数据到elementData从index 到numNew
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;//更新size
return numNew != 0;//返回结果
}

四种增加数据的方发,全部讲完了.基本上操作都差不多,计算扩容量,然后扩容,将数组中的元素,移动指定位置,把新元素要添加的位置腾出来,然后添加新的元素就好了.
今天先讲完添加.明天在接着讲剩余的三个方法
咱们书接上回继续说道一下,ArrayList的删除是怎么实现的

2.删

ArrayList删除元素的方法,比较多。我们先说两个常用的

1 .remove(int index)
//注意,返回值是被删除的那个元素
public E remove(int index) {
//检查index是否合法,不合法直接抛异常
rangeCheck(index);

modCount++;//修改次数加1
E oldValue = elementData(index);//先取出要删除的元素

int numMoved = size - index - 1;//要移动的元素的数量
if (numMoved > 0)
//将数组中的元素,从位置index+1开始到结束,向前移动到index位置
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//先将size减1,然后在将减1后的elementData[size]置空,
elementData[--size] = null; // clear to let GC do its work

return oldValue;//返回被删除的元素
}

再看看第二个删除方法:

2.remove(Object o)
/**
* 分两种情况进行删除
*/
public boolean remove(Object o) {
//第一种情况,o为空
if (o == null) {
/***
* 循环,找到从0开始,找到第一个==null的元素,将这个元素删除,
*/
for (int index = 0; index < size; index++)
if (elementData[index] == null) {//找到第一个==null的元素
fastRemove(index);//删除这个元素
return true;//删除结束,返回结果true
}
} else {//第二种情况,从0元素开始找到第一个o.equals的元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);//删除这个相同的元素
return true;//返回结果
}
}
return false;//如果两种情况下都没有找到相同的元素,则返回false,删除失败
}

我们从上边的代码可以看出,都是找到相同的元素,在删除,删除使用的是fastRemove(index)方法
那我们就来看看,他到底是怎么删除这个元素的,为什么不调用第一个删除的方法呢

private void fastRemove(int index) {
modCount++;//修改次数+1
int numMoved = size - index - 1;//要移动的元素个数
if (numMoved > 0)
//移动index后(不包括index)的全部元素,前移一位.
System.arraycopy(elementData, index+1, elementData, index,
numMoved);

//最后一位元素置空                             //
elementData[--size] = null; // clear to let GC do its work
}

我们可以看到,跟remove(index),移动数组的操作是一模一样的,只不过没有记录被删除的元素而已

3.clear()方法

也是我们非常常用的方法,这个方法就比较简单,大家思考一下,如果让你实现clear方法,你会怎么实现

public void clear() {
modCount++;//修改次数+1

// clear to let GC do its work
//循环size的元素
for (int i = 0; i < size; i++)
//将每个元素置空
elementData[i] = null;
//size更新为0
size = 0;
}

实现起来非常简单,就是时间复杂度是O(n).我们再来对比一下removeAll()方法,看看有没有什么差别

4.removeAll()
/**
* 从此列表中删除指定集合中包含的所有元素。
*/
public boolean removeAll(Collection<?> c) {
//判断传入的参数是否为空
Objects.requireNonNull(c);
return batchRemove(c, false);
}
/**
* Objects中的requireNonNull方法
*/
public static <T> T requireNonNull(T obj) {
if (obj == null)//如果为空直接抛出空指针异常
throw new NullPointerException();
return obj;
}

我们在进入batchRemove()方法,进入,进入.哈哈哈

/**
* 源码中并没有给出这个方法的注释,所以全部靠我们自己来理解,
* 我们解释两个单词的含义,方便大家理解这个方法
* complement:google翻译为:补充
* batch:google翻译为:批量
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
/**
* 这里循环直到r==size时结束,把elementData中,跟c中的不相同的数据,以此排列
* 循环结束后,w的值就是elementData跟c不同数据的个数,而r正常情况下应该等于size
*/
for (; r < size; r++)
//complement = true :如果c包含elementData[r]
//complement = false :如果c不包含elementData[r],通过removeAll传递过来的是
//false
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];

} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//以上两句的翻译大致是:保留与AbstractCollection的行为兼容性,
//即使c.contains()抛出也是如此。
//正常情况下,r是等于size的
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {//w!=size代表c和elementData的数据有部分不同,而不是全部不同
// clear to let GC do its work
////将elementData中,从w开始,到size的数据置空
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;//记录修改次数
size = w;//更新size
modified = true;//返回标志
}
}
return modified;//如果代码走到这里返回false代表批量删除失败
}

这里我们使用了一个contains方法来判断ArrayList是否包含该元素,具体是怎么实现的呢?
contains(Object o)方法是Collection接口中的一个方法在ArrayList中重写,
ArrayList中的实现如下

插入讲解: contains(Object o)
public boolean contains(Object o) {
//元素的下标是否大于等于0
return indexOf(o) >= 0;
}

通过indexOf方法来判断元素在ArrayList中的index,如果大于等于0,代表包含此元素,返回true
如果小于0,代表不包含此元素,返回fales
我们看看怎么获取元素的Index

public int indexOf(Object o) {
if (o == null) {//元素为空
for (int i = 0; i < size; i++)
if (elementData[i]==null)//找到第一个为空的元素
return i;//返回第一个为空的元素的index
} else {//否则
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))//找到元素equals的元素下标
return i;//返回这个下标
}
return -1;//如果没有找到任何相同的元素,则返回-1
}

从返回-1的情况来看,无论那种情况都没有找到与之相同的元素,所以index>=0就代表包含此元素了.
removeAll(Collection<?> c)方法就全部解析完了.这个方法非常的复杂,先是for重新排序,排序又依赖for循环判断index,嵌套for,最后又for循环将w之后的数据置空.又有非常多的判断,这是ArrayList的缺点,增删数据,非常复杂,时间复杂度很高,扩容的时候,内存开销又大,
接下来在说一个跟removeAll(Collection<?> c)完全相反的方法,

5.retainAll(Collection<?> c) ps:跟removeAll(Collection<?> c)完全相反

retain:保留,当看到这个保留两个字的时候,大家估计已经就明白了,这个方法就是保留Arraylist与c中相同的元素.
我们看看他是怎么实现的

public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}

跟removeAll一模一样,只是batchRemove传递的参数,变成了true,batchRemove方法也只有在这两个地方用到.
batchRemove的第一个for循环中

/**
* 将c中包含的元素,一次从0排列到w
*/
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];

剩余的步骤就不重复结束了,这个是非常简单实用的,我们在开发过程中,可以用到.
到此,删除的方法就介绍结束了.接下来我们介绍如何修改数据

3.改

改的方法非常简单,也非常好理解,这也是ArrayList的优势所在,时间复杂度最小O(1)只要改一次就可以了

public E set(int index, E element) {
rangeCheck(index);//判断是否下标越界

E oldValue = elementData(index);//保留要修改的元素
elementData[index] = element;//将新的元素赋值给index
return oldValue;//返回旧的index元素
}

4.查

ArrayList的查找方法也是简单到极致,一次操作就能完美达到要求,时间复杂度还是O(1)

public E get(int index) {
rangeCheck(index);//判断下标是否越界

return elementData(index);//返回指定的元素
}

是不是非常的简单,改查方式,是ArrayList最大的优势所在,如果你的使用场景,是查找和修改比较多,又对顺序有需求,那么,就非常适合ArrayList.
还有一个查找方法

/**
* 没有任何判断,直接返回对应下标元素
*/
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}

5.另类查找方法

上边在讲remove时,我们说道了另外一个方法indexOf(Object o),这个方法我们也是非常常用,来介绍一下

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;
}

返回数组中首个,也就是第一个出现的,与目标元素相同的元素下标.
跟这个方法对应的方法有

/***
* 返回数组中最后一个,与目标元素相同的元素的下标
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

到此,我把ArrayList基本常用的方法的实现原理,介绍完了,希望对你有所帮助!

  • 点赞
  • 收藏
  • 分享
  • 文章举报
zhuhewie 发布了15 篇原创文章 · 获赞 0 · 访问量 1193 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: