ArrrayList是Java中经常被用到的集合,弄清楚它的底层实现,有利于我们更好地使用它。
下图是ArrayList的UML图:
从图中我们可以看出:
1: 实现了RandomAccess接口:表面ArrayList支持快速(通常是常量时间)的随机访问。官方源码也给出了解释:(因为底层实现是一个数组,所以get()方法要比迭代器快,后面还会更有更加详细的源码解析)
2:实现了Cloneable接口,表明它支持克隆。可以调用clone()进行浅拷贝。
3:实现了Serializable接口,表明它支持序列化。
4:它还实现了List接口,并且继承自AbstractList抽象类。
代码分析部分太多了,我直接把总结弄到最上面了,可以方便查看。
总结:
① ArrayList在我们日常开发中随处可见,所以建议大家可以自己手动实现一个ArrayList,实在写不出来可以模仿一下ArrayList么。
② 由于ArryList随机存储,底层是用的一个数组作为存放元素的,所以在遍历ArrayList的时候,使用get()方法的效率要比使用迭代器的效率高。
③在ArrayList中经常使用的一个变量modCount,它是在ArrayList的父类AbstractList中定义的一个protected变量,该变量主要在多线程的环境下,如果使用迭代器进行删除或其他操作的时候,需要保证此刻只有该迭代器进行修改操作,一旦出现其他线程调用了修改modCount的值的方法,迭代器的方法中就会抛出异常。究其原因还是因为ArrayList是线程不安全的。
④ 在ArrayList底层实现中,很多数组中元素的移动,都是通过本地方法System.arraycopy实现的,该方法是由native修饰的。
⑤ 在学习源码的过程中,如果有看不懂的方法,可以自己写一个小例子调用一下这个方法,然后通过debug的方式辅助理解代码的含义。当然啦,有能力的最好自己实现一下。(不过有些方法确实设计的超级精巧,直接读代码还看不懂,只能通过debug辅助学习源代码,更别提写这些方法了。。。。)
⑥ 不过我们在操作集合的过程中,尽量使用使用基于Stream的操作,这样能够不仅写起来爽,看起来更爽!真的是谁用谁知道。简直不要太爽!
下面是源码解析的部分:
①首先我们先看一下JDK中ArrayList的属性有哪些:
private static final long serialVersionUID = 8683452581122892189L;
//默认的容量
private static final int DEFAULT_CAPACITY = 10;
//定义了一个空的数组,用于在用户初始化代码的时候传入的容量为0时使用。
private static final Object[] EMPTY_ELEMENTDATA = {};
//同样是一个空的数组,用于默认构造器中,赋值给顶层数组elementData。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//底层数组,ArrayList中真正存储元素的地方。
transient Object[] elementData;
//用来表示集合中含有元素的个数
private int size;
②看看我们的JDK中提供的构造器:(提供了三种构造器,分别是:需要提供一个初始容量、默认构造器、需要提供一个Collection集合。)
// 如果我们给定的容量等于零,它就会调用上面的空数组EMPTY_ELEMENTDATA。
// 如果大于零的话,就把底层的elementData进行初始化为指定容量的数组。
// 当然啦,如果小于零的话,就抛出了违法参数异常(IllegalArgumentException)。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 默认的情况下,底层的elementData使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
*
* 传入一个集合,首先把集合转化为数组,然后把集合的底层数组elementData指向该数组,
* 此时,底层数组有元素了,而size属性表示ArrayList内部元素的个数,所以需要把底层数组
* element的大小赋值给size属性,然后在它不等于0 的情况
* 下(也就是传进来的集合不为空),再通过判断保证此刻底层数组elementData数组的类型
* 和Object[]类型相同,如果不同,则拷贝一个Object[]类型的数组给elementData数组。
* 如果参数collection为null的话,将会报空指针异常。
*
* @param 一个Collection集合。
* @throws 如果参数collection为null的话,将会报异常(NullPointerException)
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
③缩至“最简洁”的容量:把ArrayList的底层elementData数组大小调整为size(size是ArrayList集合中存储的元素的个数)
那为什么会有这个方法呢?
因为我们在ArrayList中添加元素的时候,当ArrayList容量不足的时候,ArrayList会自动扩容,(调用的是ensureCapacityInternal()方法,这个方法后续会讲解。),一般扩充为原来容量的1.5倍,我们可能用不了那么多的空间,所以,有时需要这个来节省空间。
//modCount这个变量从字面意思看,它代表了修改的次数。实际上它就是这个意思。
//它是AbstractList中的 protected修饰的字段。
//我们首先解释一下它的含义:顾名思义,修改的次数(好像有点废话了)
//追根溯源还是由于ArrayList是一个线程不安全的类。这个变量主要是用来保证在多线程环境下使用
//迭代器的时候,同时在对集合做修改操作时,同一时刻只能有一个线程修改集合,如果多于一个
//线程进行对集合改变的操作时,就会抛出ConcurrentModificationException。
//所以,这是为线程不安全的ArrayList设计的。
//
//接着判断一下,如果ArrayList中元素的个数小于底层数组的长度,说明此时需要缩容。
//最后通过一个三位运算符判断,如果ArrayList中没有元素,则把底层数组设置为空数组。
//否则的话,就使用数组拷贝把底层数组的空间大小缩为size(元素个数)的大小。
//
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
④增加ArrayList实例的容量,如果有需要的话,以确保它至少能容纳元素的数量由传入的参数决定。
//官方的JDK中首先:需要确定一个最小的预期容量(minCapacity):
//它通过判断底层数组是否是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(也就是说是不是使用了
//默认的构造器,),如果没有使用默认的构造器的话,它的最小预期容量是0,如果使用了默认
//构造器,最小预期容量(minCapacity)为默认容量(DEFAULT_CAPA