ArrayList源码分析
2018-06-26技术共享Jervois5395°c
A+ A-前言
想要分析下源码是好事,但是如何进行分析呢?以我为例,我进行源码分析的过程如下几步:
找到类:利用IDEA找到所需要分析的类(这里就是ArrayList)
新建类:新建一个类,命名为ArrayList,将源码拷贝到该类中。因为我们在分析的过程中肯定是需要对代码进行注释和调试的,而对jdk的源码,我们没法儿在里面直接写注释以及调试的。
按照上面的方法将新建AbstractList类,并将源码拷贝过来,这是由于ArrayList中要用到AbstractList类中的变量,如果不拷贝过来就会报错。
修改类:我们刚拷贝过来的源码,肯定会报错的。报错原因比如:包名不匹配、继承的类中权限问题,因此我们需要对源码进行修改
查看代码 + 测试案例 + 断点调试:前面准备好了,就到分析的过程了。分析,不仅仅是简单的看下代码,我们需要仔细思考,且辅以相应的测试案例,甚至于进行断点跟踪查看运行过程。下面我们就对jdk1.8的源码进行分析。
ArrayList简介
ArrayList概述
ArrayList是可以动态增加和缩减的,踏实数组实现的list类。
该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity属性,表示他们所封装的数组的长度,当向ArrayList中添加数据的时候,capacity会自动增长,如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。
ArrayList的用法和Vector向类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。
ArrayList的数据结构
分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就是理解该类的思路。
ArrayList的数据结构是:
PS:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。
ArrayList源码分析
继承结构和层次关系
分析:
为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。 ArrayList实现了哪些接口?
List接口:我们会出现这样一个疑问,在查看了ArrayList的父类AbstractList也实现了List接口,那为什么子类ArrayList还是去实现一遍呢?开发这个collection 的作者Josh说:这其实是一个mistake,因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。
RandomAccess接口:这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。
Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。
Serializable接口:实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。
类中的属性
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // 版本号 private static final long serialVersionUID = 8683452581122892189L; // 缺省容量 private static final int DEFAULT_CAPACITY = 10; // 空对象数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // 缺省空对象数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 元素数组 transient Object[] elementData; // 实际元素大小,默认为0 private int size; // 最大数组容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; }
构造方法
ArrayList有三个构造方法:
无参构造方法
有参构造方法(一)
有参构造方法(二)
/** * 这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10. */ //ArrayList中储存数据的其实就是一个数组,这个数组就是elementData,在123行定义的 private transient Object[] elementData; public ArrayList() { super(); //调用父类中的无参构造方法,父类中的是个空的构造方法 this.elementData = EMPTY_ELEMENTDATA;//EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。 }
public ArrayList(int initialCapacity) { super(); //父类中空的构造方法 if (initialCapacity < 0){ //判断如果自定义大小的容量小于0,则报下面这个非法数据异常 throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); this.elementData = new Object[initialCapacity]; //将自定义的容量大小当成初始化elementData的大小 } }
//这个构造方法不常用,举个例子就能明白什么意思 /* Strudent exends Person ArrayList<Person>、 Person这里就是泛型 我还有一个Collection<Student>、由于这个Student继承了Person,那么根据这个构造方法,我就可以把这个Collection<Student>转换为ArrayList<Sudent>这就是这个构造方法的作用 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); //转换为数组 size = elementData.length; //数组中的数据个数 if (elementData.getClass() != Object[].class) //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么久需要使用ArrayList中的方法去改造一下。 elementData = Arrays.copyOf(elementData, size, Object[].class); }
总结:arrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。
核心方法
add()方法(有四个):
boolean add(E)//默认直接在末尾添加元素
void add(int,E);在特定位置添加元素,也就是插入元素
分析:ensureCapacityInternal(xxx); 确定内部容量的方法
private void ensureCapacityInternal(int minCapacity) { if (elementData == EMPTY_ELEMENTDATA) { //看,判断初始化的elementData是不是空的数组,也就是没有长度 //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是带这里,还没有真正的初始化这个elementData的大小。 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用 ensureExplicitCapacity(minCapacity); }
ensureExplicitCapacity(xxx);
private void ensureExplicitCapacity(int minCapacity) { modCount++; /*minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。这里有的同学就会模糊minCapacity到底是什么呢,这里给你们分析一下 第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会给 将minCapacity=10,到这一步为止,还没有改变elementData的大小。 第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length 不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。 */ if (minCapacity - elementData.length > 0) //arrayList能自动扩展大小的关键方法就在这里了 grow(minCapacity); }
grow(xxx);arrayList核心的方法,能扩展数组大小的真正秘密。
private void grow(int minCapacity) { int oldCapacity = elementData.length; //将扩充前的elementData大小给oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity if (newCapacity - minCapacity < 0)//这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity newCapacity = hugeCapacity(minCapacity); //新的容量大小已经确定好了,就copy数组,改变容量大小咯。 elementData = Arrays.copyOf(elementData, newCapacity); }
hugeCapacity();
//这个就是上面用到的方法,很简单,就是用来赋最大值。 private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) throw new OutOfMemoryError(); //如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。 //Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639 也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE :MAX_ARRAY_SIZE; }
public void add(int index, E element) { rangeCheckForAdd(index);//检查index也就是插入的位置是否合理。 //跟上面的分析一样,具体看上面 ensureCapacityInternal(size + 1); //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位, System.arraycopy(elementData, index, elementData, index + 1,size - index); //在目标位置上存放元素 elementData[index] = element; size++;//size增加1 }
rangeCheckForAdd(index)
private void rangeCheckForAdd(int index) { if (index > size || index < 0) //插入的位置肯定不能大于size 和小于0,如果是,就报这个越界异常 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
System.arraycopy(…):就是将elementData在插入位置后的所有元素往后面移一位。
删除方法:
remove(int):通过删除指定位置上的元素。
clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear。
public E remove(int index) { rangeCheck(index);//检查index的合理性 modCount++;//这个作用很多,比如用来检测快速失败的一种标志。 E oldValue = elementData(index);//通过索引直接找到该元素 int numMoved = size - index - 1;//计算要移动的位数。 if (numMoved > 0) //这个方法也已经解释过了,就是用来移动元素的。 System.arraycopy(elementData, index+1, elementData, index,numMoved); //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。 elementData[--size] = null; // clear to let GC do its work //返回删除的元素。 return oldValue; }
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
总结:remove函数用户移除指定下标的元素,此时会把指定下标到数组末尾的元素向前移动一个单位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。
set()方法:
public E set(int index, E element) { // 检验索引是否合法 rangeCheck(index); // 旧值 E oldValue = elementData(index); // 赋新值 elementData[index] = element; // 返回旧值 return oldValue; }
PS:设定指定下标索引的元素值
indexOf()方法:
// 从首开始查找数组里面是否存在指定元素 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; }
PS:从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找
get()方法:
public E get(int index) { // 检验索引是否合法 rangeCheck(index); return elementData(index); }
PS:get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下:
E elementData(int index) { return (E) elementData[index]; }
PS:返回的值都经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节。
总结
arrayList可以存放null。
arrayList本质上就是一个elementData数组。
arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。
arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。
arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果
arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
本文来源:GageChan's blog