您的位置:首页 > 编程语言 > Java开发

JDK源码解析集合篇--ArrayList全解析

2017-08-31 11:32 676 查看
对于一个集合的使用,我们首先关注的是:

1、 增删改查的特点(时间复杂度是怎样的) 适用于随机访问

2、是否允许空 允许为空

3、是否允许重复数据 允许

4、是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致 有序

5、是否线程安全 非线程安全

ArrayList实现原理

ArrayList就是一个以数组形式实现的集合,但是它实现了长度可变。我们可以看其源码属性:



ArrayList是基于数组的一个实现,elementData就是底层的数组,size:ArrayList里面元素的个数,这里要注意一下,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1 。其初始容量为10,当大于10时,进行扩容,在后边详细分析。

添加元素

当我们向一个arraylist添加一个元素时:我们直接看其源码,是怎么操作的:

public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}


上面代码中有ensureCapacityInternal方法,是确保容量够用,若不够用就扩容。

private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//将现在的容量与我们需要的最小容量size + 1进行比较,选大的
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//看是否需要扩容
ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;  //改变modCount  是为迭代器抛出异常做判断的变量

// overflow-conscious code
//如果现有的容量不满足需要的最小容量,需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}


扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因。本文是基于JDK1.8的,具体的:

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;  //原先的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);  //容量变为原来的1.5倍
//是否满足需要的最小容量,若不满足,则扩容为需要的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//是否是在MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 内,不在的话设未MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//将原数组元素拷贝到新的扩容后的数组中    是利用System.arraycopy实现的
elementData = Arrays.copyOf(elementData, newCapacity);
}


这里要注意扩容后的新容量为什么要变为原来的1.5倍:这是因为扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。

1、如果一次性扩容扩得太大,必然造成内存空间的浪费

2、如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比价耗费性能的一个操作。

添加操作还有一个方法,可以做到在任意位置添加:

public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1);  // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}


看到插入的时候,按照指定位置,把从指定位置开始的所有元素利用System.arraycopy方法做一个整体的复制,向后移动一个位置(当然先要用ensureCapacity方法进行判断,加了一个元素之后数组会不会不够大),然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。

此复杂度较高,所以ArrayList不适合进行随机插入和删除,而可以利用数组下标在O(1)时间内实现随机访问。

删除元素

ArrayList支持两种删除方式:

1、按照下标删除

2、按照元素删除,这会删除ArrayList中与指定要删除的元素匹配的第一个元素

按照下标删除,返回删除的元素:

/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
//进行边界检查  看是否超过现在的容量
rangeCheck(index);

modCount++;    //为迭代器抛出异常使用  其结构被改变的次数
E oldValue = elementData(index);
//要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
//将原数组的从index+1开始的numMoved个数拷贝到index开始的
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将最后一个数设为null。完成拷贝工作
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}


按元素删除操作,返回是否删除成功:

//其实质也是通过遍历寻找第一个此元素,完成删除
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;
}

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
}


获取/改变元素

因为是利用动态数组实现的,所以随机访问肯定是其最大的优点:

//获取index处的元素
public E get(int index) {
//检查是否在范围内
rangeCheck(index);

return elementData(index);
}
//改变index处的元素值
public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
//获取某一元素的下标index
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;
}


ArrayList总结

ArrayList的优缺点从上边的源码的分析就能明显看到其优缺点:

1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

不过ArrayList的缺点也十分明显:

1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,ArrayList比较适合顺序添加、随机访问的场景。

ArrayList和Vector的区别

Vector底层也是通过动态数组实现的,也允许插入null。ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:

List<String> synchronizedList = Collections.synchronizedList(list);


最好的方法是使用并发包中的集合。另一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样,区别在于:

1、Vector是线程安全的,ArrayList是线程非安全的

2、Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码是这样的:

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容方法:如果指定了capacityIncrement 因子,就在原容量上+此因子
//否则:就变为原来的2倍   这和ArrayList是不同的
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}


另外:

看到ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
//ObjectOutputStream 的序列化对象方法  先序列化无transient修饰的
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
//然后自定义序列化  只序列化元素  size+元素值
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}


每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

1、加快了序列化的速度

2、减小了序列化之后的文件大小

到这里,我们就完成了ArrayList的源码分析,下一篇,重点分析LinkedList的实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息