在前面的几节里,本教程从整体架构上去把握了JDK中的集合框架,并简单分析了其中Collection组的*接口,知道Collection接口的常见直接子接口有List、Set和Queue,并就这三个子接口的独有特性进行了简单地分析和比较。
本篇教程将会对实际编程中使用最频繁、最简单地ArrayList进行讲解,我先给出一个该类的类层次结构图。
从上面的结构图可以看出,集合框架的主题架构其实是以类为主体的,而不是接口。如果知道类、抽象类和接口之间的区别与联系,就能明白这里引入抽象类的原因。简单地说,如果不引入抽象类,每个具有Collection接口属性的类别都必须自己去实现该接口中的10多个方法,通过让类别直接继承自AbstractCollection这个抽象类,去除了重复代码。
首先,我们来看一下AbstractCollection这个抽象类的部分源码,如下:
public abstract class AbstractCollection<E> implements Collection<E> {
public boolean isEmpty() {
return size() == 0;
}
public boolean add(E e) {
throw new UnsupportedOperationException();
}
}
此抽象类就是Collection接口的实现,并实际实现了其中的某些方法,而且作为一个抽象类是不能直接用来实例化对象。
其次,再看看AbstractList这个抽象类的部分源码,如下:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public boolean add(E e) {
add(size(), e);
return true;
}
}
此抽象类继承了AbstractCollection抽象类,并且实现了List接口,这就使得此类同时具备了普通Collection和特定的List类型集合的属性。注意:此类的subList方法返回的List<E>类型是两个内部类中的一个,要么是RandomAccessSubList<E>,要么是SubList<E>。
最后,我们再来仔细看看ArrayList的源码,部分源码如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
private transient Object[] elementData;
}
这个ArrayList作为一个非抽象方法,已经实现了抽象父类和父接口中的所有方法。整体来说,并没有太多新的方法和特性加入,但其中值得关注的点不少,现在我们就来一一剖析。
第一个不寻常的地方在于ArrayList实现了Serializable接口,但是该类的重要成员elementData却使用transient修饰符。transient修饰符的作用正是让被其修饰的变量不参与正常的序列化中,那么ArrayList是如何实现Serializable的呢?秘密藏在该类的两个方法:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
}
我们知道对于实现了Serializable接口的类,我们可以直接使用ObjectOutputStream和ObjectInputStream进行对象的序列化和反序列化。当我们的类中包含有这两个方法时,ObjectOutputStream的writeObject方法就会调用类中的writeObject方法。注意:如果的看得够仔细,你会发现writeObject方法是私有的,同时该方法在Object中并不存在,那么ObjectOutputStream作为外部类是如何找到writeObject方法并调用成功的呢?如果你知道Java的反射机制的话,我相信你可能已经猜到了,此处正是使用的反射机制。这里之所以如何操作是为了效率,毕竟只需要序列化elementData中size大小的变量,而不是capacity大小的变量。
第二个不寻常的地方在于ArrayList类的clone()方法,从类的定义中可以看到该类实现了Cloneable接口,根据Cloneable接口的语义,ArrayList类实现了自己的clone()方法,我们看一下源码:
public Object clone() {
try {
@SuppressWarnings("unchecked")
ArrayList<E> v = (ArrayList<E>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}
源码面前无秘密,可以看到方法中对于保存ArrayList内部数据的数组变量elementData是做了复制的,但是并没有针对数组变量本身再进行递归复制,所以在注释中写明了此方法是一个浅复制。
第三个不寻常的地方在于ArrayList类中含有一个名为modCount的变量,名字其实是modifyCount的缩写,表示ArrayList对象被修改的次数。而且这个modCount记录的只是修改size变量的次数,对于ArrayList对象中变量的替换是不会改变modCount的值。那么ArrayList类为什么要辛苦地去维护这个modCount变量值呢?如果你写过遍历ArrayList集合的同时去删除集合中某些元素的代码,并且你不是使用的迭代器方式的话,那么你一定遇到过ConcurrentModificationException异常。背后的原理就在于remove方法会修改modCount的值,而整个foreach循环过程不允许modCount值发生改变。
第四点来看一下ArrayList的内存数组增长机制,ArrayList模拟可动态增长的数组,其采用的内存增长机制也是很多笔试和面试题喜欢设计的话题,我们直接来看一下源码:
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);
}
从源码可以出到,通常情况下就是一个50%的增长幅度,但也可以直接设定具体增长的目标值。同时,某些JVM实际可分配的最大数组大小并不能达到Integer.MAX_VALUE,而是Integer.MAX_VALUE - 8。
关于ArrayList<E>还有比较多的知识点,限于篇幅就不一一讲解了,像其中迭代器模式的实现,内部类的使用等就暂时先跳过啦,以后以机会再专门讲解。最后提一下ArrayList<E>实现的RandomAccess这个接口,它是一个标记接口,本身不包含任何成员,用来标记某些集合类具有随机访问性,从而在应用泛型算法可以考虑利用该特点来提高算法性能。
本系列文档会在本人的微信公众号发布,欢迎大家扫码关注。