1 ArrayList的数据结构
2 ArrayList的线程安全性
3 ArrayList的继承关系
4 ArrayList的主要成员变量
5 ArrayList的构造方法
5.1 无参构造方法
5.2 int类型参数构造方法
5.3 Collection类型构造方法
6 ArrayList的扩容机制
6.1 add 方法
6.2 ensureCapacityInternal() 方法
6.3 ensureExplicitCapacity() 方法
6.4 grow() 方法
6.5 hugeCapacity() 方法
7 各种方法
7.1 ArrayList的set(int index,E element)方法
7.2 ArrayList的indexOf(Object o)方法
7.3 ArrayList的get(int index)方法
7.4 ArrayList的remove(int index)方法
ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。
对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。
具体举例说明:在单线程运行的情况下,如果Size = 0,添加一个元素后,此元素在位置 0,而且Size=1;而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增 加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而Size却等于 2。这就是“线程不安全”了。
如果非要在多线程的环境下使用ArrayList,就需要保证它的线程安全性,通常有两种解决办法:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();对ArrayList进行调用即可。
ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。
当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
ArrayList的底层数据结构,只是一个对象数组,用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候此字段是不会被序列化的。
实际ArrayList中存放的元素的个数,默认时为0个元素。
ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
public class ArrayList extends AbstractList
implements List, 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;
}
对于无参构造方法,将成员变量elementData的值设为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
public ArrayList() {
// 无参构造函数,设置元素数组为空
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
参数为希望的ArrayList的数组的长度,initialCapacity。首先要判断参数initialCapacity与0的大小关系:
如果initialCapacity大于0,则创建一个大小为initialCapacity的对象数组赋给elementData。
如果initialCapacity等于0,则将EMPTY_ELEMENTDATA赋给elementData。
如果initialCapacity小于0,抛出异常(非法的容量)。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { // 初始容量大于0
this.elementData = new Object[initialCapacity]; // 初始化元素数组
} else if (initialCapacity == 0) { // 初始容量为0
this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
} else { // 初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
第一步,将参数中的集合转化为数组赋给elementData;
第二步,参数集合是否是空。通过比较size与第一步中的数组长度的大小。
第三步,如果参数集合为空,则设置元素数组为空,即将EMPTY_ELEMENTDATA赋给elementData;
第四步,如果参数集合不为空,接下来判断是否成功将参数集合转化为Object类型的数组,如果转化成Object类型的数组成功,则将数组进行复制,转化为Object类型的数组。
public ArrayList(Collection extends E> c) { // 集合参数构造函数
elementData = c.toArray(); // 转化为数组
if ((size = elementData.length) != 0) { // 参数为非空集合
if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
} else { // 集合大小为空,则设置元素数组为空
this.elementData = EMPTY_ELEMENTDATA;
}
}
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
//添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。
如果调用 ensureCapacityInternal() 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码!
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
我们来仔细分析一下:
直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
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);
}
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.
我们再来通过例子探究一下grow() 方法 :
这里补充一点比较重要,但是容易被忽视掉的知识点:
①java 中的 length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
②java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.
③java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
set(int index, E element)方法的作用是指定下标索引处的元素的值。在ArrayList的源码实现中,方法内首先判断传递的元素数组下标参数是否合法,然后将原来的值取出,设置为新的值,将旧值作为返回值返回。
public E set(int index, E element) {
// 检验索引是否合法
rangeCheck(index);
// 旧值
E oldValue = elementData(index);
// 赋新值
elementData[index] = element;
// 返回旧值
return oldValue;
}
indexOf(Object o)方法的作用是从头开始查找与指定元素相等的元素,如果找到,则返回找到的元素在元素数组中的下标,如果没有找到返回-1。与该方法类似的是lastIndexOf(Object o)方法,该方法的作用是从尾部开始查找与指定元素相等的元素。
查看该方法的源码可知,该方法从需要查找的元素是否为空的角度分为两种情况分别讨论。这也意味着该方法的参数可以是null元素,也意味着ArrayList集合中能够保存null元素。方法实现的逻辑也比较简单,直接循环遍历元素数组,通过equals方法来判断对象是否相同,相同就返回下标,找不到就返回-1。这也解释了为什么要把情况分为需要查找的对象是否为空两种情况讨论,不然的话空对象调用equals方法则会产生空指针异常。
// 从首开始查找数组里面是否存在指定元素
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;
}
get(int index)方法是返回指定下标处的元素的值。get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0)。如果所引致合法,则调用elementData(int index)方法获取值。在elementData(int index)方法中返回元素数组中指定下标的元素,并且对其进行了向下转型。
public E get(int index) {
// 检验索引是否合法
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
remove(int index)方法的作用是删除指定下标的元素。在该方法的源码中,将指定下标后面一位到数组末尾的全部元素向前移动一个单位,并且把数组最后一个元素设置为null,这样方便之后将整个数组不再使用时,会被GC,可以作为小技巧。而需要移动的元素个数为:size-index-1。
public E remove(int index) {
// 检查索引是否合法
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 赋值为空,有利于进行GC
elementData[--size] = null;
// 返回旧值
return oldValue;
}