集合之ArrayList
这几天在看jdk源码,觉得集合是比较重要的基础框架,所以对它进行了一部分的学习和总结。也发现了不少问题。在此进行一个记录。
ArrayList集合的主要继承实现关系
1.所有集合的顶级接口类是Collection接口,该接口继承了Iterable接口。2.AbstractCollection实现了Collection接口,List接口继承Collection接口。
3.AbstaractList继承了AbstractCollection抽象类,实现了List接口。而我们的ArrayList接口则在继承AbstractList抽象类的同时,实现了List接口。
关系图如下:
根据
在这张图中我们会发现一件很有意思的事,我们的ArrayList集成了AbstractList的同时,还实现了List接口。如果小伙伴们观察仔细的话会发现,AbstractList已经实现了List接口,为什么ArrayList还要再实现呢?这个本人也没有琢磨清楚,有人说这其实是作者的一个错误,菜鸟也不敢问…如果有大佬知道的还请赐教哈。我们接着简单介绍一下相关类和接口,重点聊聊ArrayList的实现。
Iterable
iterable接口定义了 三个方法。
1.Iterator iterator
该方法规定所有集合必须要实现迭代。
2.foreach方法,该方法是一个默认方法,为jdk1.8新特性。
Collection
Collection接口继承了Iterable接口,定义了集合类的基础方法,如长度,是否为空,增删改查等,清空等方法。
AbstractCollection
AbstractCollection是一个抽象类,该类新增以及实现了部分方法。如:isEmpty,contains 以及集合转化为数组的等方法。
List接口
AbstractList继承了AbstractCollection
该类对AbstractCollection进行了扩展以及实现。
ArrayList
我们都知道,ArrayList底层实现是数组,接下来我们根据源码解读一下。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
由上面代码我们可以看出ArrayList除了继承了AbstractList,还实现了List接口,RandomAccess接口,Cloneable接口以及Serializable接口。其中Serializable接口是用于需要远程传输,将对象持久化存储等。
private static final long serialVersionUID = 8683452581122892189L;
结合serialVersionUID ,我们就可以使用,在这里我们需要知道的是,如果当前类实现了序列化接口,即使我们不提供默认的serialVersionUID 也是可以的,因为如果没有给出Serializable会有一个默认值,但是这个默认值在实际使用时可能会出现对象序列化失败的现象,所以这里提供了一个。
我们接着往下看:
private static final int DEFAULT_CAPACITY = 10;
这句话定义了ArrayList的默认长度,若初始化时未提供长度,则初始化为默认的长度。
//实例化一个空数组,用于对比集合是否为空时用到
private static final Object[] EMPTY_ELEMENTDATA = {};
//实例化一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//真正存储数据的数组,为一个Object类型的数组。transient 关键字表示该属性不进行序列化。
transient Object[] elementData; // non-private to simplify nested class access
//集合的长度
private int size;
我们接下来看看构造函数:
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_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;
}
}
第一种构造函数需要用户初始化集合长度,如代码所见,用户在创建一个集合时需要传入一个int类型的名为initialCapacity的代表初始化长度的参数。如果用户传的初始化长度大于零,则elementData 指向一个长度为initialCapacity的数组,若为零,则指向上面定义的一个空数组。
第二个构造函数是一个无参构造函数,该构造函数直接创建一个空数组。
第三个构造函数则接收一个集合,系统将该集合转换成数组后直接赋值到elementData
由上面所知,当JVM执行了 List list = new ArrayList()时,调用了ArrayList的无参构造函数,指向了ArrayList中默认的空数组。当然也可以使用带参构造方法创建,总之,此时ArrayList底层已经维护了一个数组。
我们知道如何创建ArrayList集合了,接下来要做的就是往集合中添加数组,我们来看看添加方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
由上面代码可知,elementData[size++] = e;即为往集合中添加内容的操作,但是众所周知,数组长度是不可边的,刚才我们创建了一个空数组,长度为0,现在要往size++角标赋值,这样会导致异常呀?相信大家都发现了在添加数组前还调用了ensureCapacityInternal方法,将数组此时长度加一后传到了该参数,所以我们知道了ensureCapacityInternal方法为ArrayList扩容的方法。我们进入方法一探究竟:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
从上面的代码我们可以知道,该方法是主要用于计算需要扩容的长度。当elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,表示当前对象内容为空,还没有值,此时通过对比当前size+1与默认长度的大小,确定需要扩充的长度。当大于10时,扩充size+1个长度,当小于10时,扩充为默认长度10。
计算扩充长度后又调用了ensureExplicitCapacity方法。我们继续进入
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
1.modCount是从父类继承过来的参数,用于标识当前对象修改的次数,通过对比该值,可以防止在遍历集合时删除集合中的元素。
2.对集合长度与扩充的长度进行对比,如果需要扩充的长度小于集合原有的长度,就不再扩充。
3.调用grow方法进行扩充。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
minCapacity 为需要扩充的长度,该方法执行流程如下:
1.获取集合当前大小
2.计算出新的集合大小,我们通过代码可以看出,新的长度为当前集合长度+当前集合长度>>1,使用位运算,可以提高计算效率,当前集合长度>>1相当于 当前集合长度/2,所以我们可以得出,ArrayList实际扩充为原来的1.5倍。这个答案是这样得出来的。
3.如果计算得出的扩充长度小于需要扩充的长度,则取传递的扩充数。
4.当扩充长度大于MAX_ARRAY_SIZE ,也就是大于Int.MAX_VALUE-8,则调用hugeCapacity函数继续进行计算:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//三元运算符判断,当需要扩充的容量大于最大的值时,只扩充到Integer.MAX_VALUE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
5.将当前集合的数据拷贝一份。
现在我们知道了,每一次往集合中添加数据,都需要查看空间是否足够,如果不够,会进行一次扩容,扩容后的集合长度空间大小为原来的1.5倍。
在这里可以总结出一个小技巧:
当我们需要往集合中添加大量数据时,会十分耗费性能,我们是否可以一次性让它扩容完,以提高效率呢?
答案是可以的,ArrayList类为我们提供了ensureCapacity方法。
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
ensureCapacity接收一个需要扩充的长度参数,方法在底层调用了ensureExplicitCapacity方法,直接扩充到指定大小。那么现在我们来验证一下,哪种方法效率会更高呢?
@Test
public void test(){
//1.创建一个ArrayList
List<String> list = new ArrayList();
String line = "test======================";
int number = 100;
long start = System.currentTimeMillis();
for (int i = 0;i < number;i++){
list.add(line);
}
long stop = System.currentTimeMillis();
System.out.println("直接往List中相加耗时:"+(stop-start));
list.clear();
((ArrayList<String>) list).ensureCapacity(number);
start = System.currentTimeMillis();
for (int i = 0;i < number;i++){
list.add(line);
}
list.remove(1);
System.out.println(list.size());
stop = System.currentTimeMillis();
System.out.println("先ensureCapacity再往List中相加耗时:"+(stop-start));
}
我们下面再将number值设置为10000000结果如下:
我们可以看到速度快了许多。
除此之外,ArrayList还有一个方法值得了解一下:
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
该方法用于去除多余的空间,因为ArrayList扩容,每次扩大1.5倍+1。这个方法是用于去除多余的空间。在内存空间紧张的时候使用.
其他问题:
1.为什么数组最大长度是Integer.MAX_VALUE - 8?
答:在数组的对象头里有一个_length字段,记录数组长度,所以为需要-8。
2.在Collection中我们发现toArray()方法代码如下:
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
r[i] = it.next();
}
return it.hasNext() ? finishToArray(r, it) : r;
}
为什么是使用for循环,而不是iterator(如下)进行迭代呢?
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
while ( it.hasNext()) {
r[i] = it.next();
}
return it.hasNext() ? finishToArray(r, it) : r;
}
答:是因为如果使用了迭代器,那么当一个线程在进行转换操作时,如果另一个线程往集合里添加元素,此时数组r的长度等于添加元素以前的长度,这样会报角标越界异常。所以这是为了线程安全做的实现。