ArrayList 简单的源码解析(Oracle JDK 1.8.0_211)
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方法,这个大家应该都不陌生吧
- add(E e)
- add(int index, E element)
- addAll(Collection<? extends E> c)
- 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基本常用的方法的实现原理,介绍完了,希望对你有所帮助!
- 点赞
- 收藏
- 分享
- 文章举报
- LinkedList简单的源码介绍(JDK1.8.0_211)
- Jdk1.8 Collections Framework源码解析(1)-ArrayList
- java ArrayList 源码解析(jdk1.6)
- 【源码解析】JDK源码之ArrayList
- ArrayList 源码解析 及其扩展(jdk1.7)
- ArrayList源码解析-JDK1.8
- JDK源码解析之ArrayList和LinkedList
- JDK之ArrayList源码解析
- 【JDk源码解析之一】ArrayList源码解析
- javase基础回顾(一)ArrayList深入解析 解读ArrayList源代码(JDK1.8.0_92)
- ArrayList源码解析(基于JDK1.6)
- ArrayList源码简单解析
- 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析
- JDK源码之ArrayList源码解析
- JDK1.8源码解析——java.util.ArrayList 类(数组实现)
- JDK 8源码解析——ArrayList和LinkedList迭代性能比较
- 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析
- 给jdk写注释系列之jdk1.6容器(1)-ArrayList源码解析
- ArrayList源码解析(jdk1.8)
- ArrayList源码解析(基于JDK1.7)