继承结构和层次关系
我们看一下ArrayList的继承结构:
ArrayList extends AbstractList
AbstractList extends AbstractCollection
所有类都继承Object 所以ArrayList的继承结构就是上图这样。
总体介绍
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。
size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。
为追求效率,ArrayList没有实现同步(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。
类中的属性
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有三个构造方法:
无参构造方法
/**
* Constructs an empty list with an initial capacity of ten. 这里就说明了默认会给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的大小
}
方法剖析
set()
既然底层是一个数组ArrayList的set()方法也就变得非常简单,直接对数组的指定位置赋值即可。
public E set(int index, E element) {
rangeCheck(index);//检验索引是否合法
E oldValue = elementData(index);//旧值
elementData[index] = element;//赋新值
return oldValue;//返回旧值
}
get()
get()方法同样很简单,唯一要注意的是由于底层数组是Object[],得到元素后需要进行类型转换。
public E get(int index) {
rangeCheck(index);// 检验索引是否合法(只检查是否大于size,而没有检查是否小于0)
return (E) elementData[index];//注意类型转换
}
add()方法//默认直接在末尾添加元素
/**
* 把元素添加到集合的末尾
*/
public boolean add(E e) {
//确定数组容量是否足够,size是数据个数,所以+1.
ensureCapacityInternal(size + 1);
//在数组中正确的位置上添加元素e,并size++
elementData[size++] = e;
return true;
}
ensureCapacityInternal(xxx); 确定内部容量的方法
//用于确定数组容量
private void ensureCapacityInternal(int minCapacity) {
//判断初始化elementdata是否为一个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是,相当于是空数组没有长度,则获取“默认的容量”和“传入参数”两者之间的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//确认实际的容量,判断是否需要进行扩容操作
ensureExplicitCapacity(minCapacity);
}
ensureExplicitCapacity(xxx);
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
//记录修改次数
modCount++;
//如果最小大小 减 数组长度 大于0 -> 进行数组扩容
if (minCapacity - elementData.length > 0)
//实际进入扩容机制
grow(minCapacity);
}
void add(int,E);在特定位置添加元素,也就是插入元素add(int index, E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度。
/**
* 在此列表中的指定位置插入指定的元素。
*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}
rangeCheckForAdd(index)
private void rangeCheckForAdd(int index) {
if (index > size || index < 0) //插入的位置肯定不能大于size 和小于0
//如果是,就报这个越界异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
grow(xxx); arrayList核心的方法,能扩展数组大小的真正秘密。
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
//将扩充前的elementData大小给oldCapacity
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity/2
//newCapacity就是1.5倍的oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);
//检查新容量是否大于最小需要容量,若小于最小需要容量,那么就把最小需要容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
/**
* 检查新容量是否超出了ArrayList所定义的最大容量,
* 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
* 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
*/
if (newCapacity - MAX_ARRAY_SIZE > 0)
//比较minCapacity和 MAX_ARRAY_SIZE
newCapacity = hugeCapacity(minCapacity);
//新的容量大小已经确定好了,就copy数组,改变容量大小
elementData = Arrays.copyOf(elementData, newCapacity);
}
hugeCapacity();
//这个就是上面用到的方法,很简单,就是用来赋最大值。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
由于Java GC自动管理了内存,这里也就不需要考虑源数组释放的问题。
空间的问题解决后,插入过程就显得非常简单。
总结:
正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。
当我们调用add方法时,实际上的函数调用如下:
说明:程序调用add,实际上还会进行一系列调用,可能会调用到grow,grow 会调用hugeCapacity。
addAll()
addAll()方法能够一次添加多个元素,根据位置不同也有两个把本,一个是在末尾添加的addAll(Collection<? extends E> c)方法,一个是从指定位置开始插入的addAll(int index, Collection<? extends E> c)方法。跟add()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。
addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。
remove()
remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。
remove(int):删除指定位置上的元素
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;
}
remove(Object):这个方法可以看出来,arrayList是可以存放null值得
//通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素,
//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值
public boolean remove(Object o) {
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;
}
关于Java GC这里需要特别说明一下,有了垃圾收集器并不意味着一定不会有内存泄漏。对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋null值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。
clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear
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,可以作为小的技巧使用。
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;
}
1)arrayList可以存放null。2)arrayList本质上就是一个elementData数组。3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法。4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全删除集合中的元素。5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。