现在,我怕的并不是那艰苦严峻的生活,而是不能再学习和认识我迫切想了解的世界。对我来说,不学习,毋宁死。——罗蒙诺索夫
本篇分析ArrayList的源码,在分析之前 需要先明白数组的概念。
数组是物理存储连续、逻辑存储连续的顺序表,这种存储方式的优点是查询的时间复杂度为O(1),通过首地址和偏移量就可以直接访问到元素,缺点是不利于修改,插入和删除的时间复杂度最坏能达到O(n)。由于数组直接操作内存,所以数组的性能要比集合类更好一点,这是使用数组的一大优势。但是,数组在初始化必须制定数组大小,并且在后续操作中不能更改数组的大小。
ArrayList它的底层是基于数组实现的,封装了一个Object[]类型的数组,长度可以动态的增长,因此它具有数组的一些特点。它是线程不安全的。
继承关系
ArrayList<E> extends AbstractList<E>
实现接口
List<E>, RandomAccess, Cloneable, java.io.Serializable
//实现了RandomAccess接口,支持快速随机访问
//实现了Cloneable接口,能够被克隆
//实现了Serializable接口,支持序列化
成员变量
//可序列化版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
//实例化一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空数组
//与EMPTY_ELEMENTDATA数组的区别在于当第一个元素被加入进来的时候它知道如何扩张;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//然后是一个object数组elementData。
//这个就是最重要的成员了,通过注释我们可以看到这表示这个数组用来存储我们的数据。
//也就是说,我们代码中的add的数据都会放在这个数组里面
transient Object[] elementData; // 不会被序列化
//ArrayList实际元素容量,和存放ArrayList元素的数组长度可能相等,也可能不相等
private int size;
构造函数
//无参构造函数
//构造一个初始容量为10的空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//根据参数的大小做为容量来实例化数组对象,当参数小于0时,抛异常。当参数等于0时,用空的常量数组对象
//EMPTY_ELEMENTDATA来初始化底层数组elementData
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);
}
}
//将集合转为ArrayList,首先先调用toArray()方法转为数组赋值给elementData
//如果集合不为空,先进行是否是object[]类型,如果不是,转换成Object[]类型。
//如果集合为空,则就将空的常量数组对象EMPTY_ELEMENTDATA赋给了elementData
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;
}
}
从构造函数中可以看出,常量EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了初始化elementData的。
如果为无参构造函数,使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA;如果为含参构造函数,使用EMPTY_ELEMENTDATA:
首先我们先来看看add方法,源码如下
public boolean add(E e) {
//添加之前先检查容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//在数组末尾添加元素,并让size+1
elementData[size++] = e;
return true;
}
在添加元素之前要先检查容量,因此我们看看ensureCapacityInternal这个函数,源码如下
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
可以看出ensureCapacityInternal函数调用了ensureExplicitCapacity方法,而calculateCapacity方法的返回值做为参数又传给了ensureExplicitCapacity方法。我们再来看看calculateCapacity方法,源码如下
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA,设置数组最小容量(>=10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
接下来我们再来看看ensureExplicitCapacity函数,源码如下
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果最小容量大于数组长度就扩容数组
if (minCapacity - elementData.length > 0)
//扩容方法
grow(minCapacity);
}
我们继续看看grow方法,看看是如何进行扩容的,源码如下
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容公式,扩展到原来数组长度的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果newCapacity扩展的过小。则应该至少扩张到所需的空间大小minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//MAX_ARRAY_SIZE 是数组的最大长度
//private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//newCapacity扩张的过大,如果过大,则用Integer.MAX_VALUE来代替
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//将原来的数组中的元素复制扩展到大小为newCapacity的新数组中,并返回这个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
添加方法可以分为两种方式,不指定位置添加和指定位置添加,接下来我们分析下指定位置添加方法,
源码如下
public void add(int index, E element) {
//位置有效性检查,必须在数组的容量范围内
rangeCheckForAdd(index);
//判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//数组内位置为 index 到 (size-1)的元素往后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
接下来我们看看get方法,源码如下
public E get(int index) {
//先进行index合法性判断,如果>=size,直接抛出数组越界异常
rangeCheck(index);
//如果合法直接返回index位置的元素
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
接下来我们在看看修改方法set,源码如下
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
修改方法,就是替换指定位置上的元素
我们在继续看看删除方法,删除操作也存在两种方式:一种是删除指定位置的元素,另一种是删除指定元素。首先我们来看看第一种,源码如下
public E remove(int index) {
rangeCheck(index);
modCount++;
//取得该位置元素
E oldValue = elementData(index);
int numMoved = size - index - 1;
//将index后面的元素向前挪一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//清空末尾元素让 GC 生效,并修改数组中的元素个数
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
接下来我们在看看第二种删除方式,源码如下
public boolean remove(Object o) {
//无论是指定对象o是否为null,都是在ArrayList中找到与此第一个相等的元素的位置,然后调用fastRemove(index)
//来进行移除;如果没有找打指定对象o的位置,则返回false,表示没有移除成功
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
我们继续看看clear方法,源码如下
public void clear() {
modCount++;
//直接将数组中的所有元素设置为null即可,这样便于垃圾回收
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
最后我们再来研究一下,为什么ArrayList源码中,为什么又要用transient修饰elementData呢?那么它又是怎么实现序列化的呢?
先回答第一个问题,
回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数,如果直接序列化elementData数组,那么就会浪费空间,特别是当元素个数非常多时,这种浪费是非常不合算的。所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。源码如下
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
//循环时是使用i<size而不是 i<elementData.length
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
现在来回答第二个问题,
如果一个类不仅实现了Serializable接口,而且定义了 readObject(ObjectInputStream in)和 writeObject(ObjectOutputStream out)方法,那么将按照如下的方式进行序列化和反序列化:
ObjectOutputStream会调用这个类的writeObject方法进行序列化,ObjectInputStream会调用相应的readObject方法进行反序列化。
那么ObjectOutputStream又是如何知道一个类是否实现了writeObject方法呢?又是如何自动调用该类的writeObject方法呢?
答案是:是通过反射机制实现的。
好了,ArrayList源码就分析到这了,希望对大家有帮助。
参考资料