前言
为啥要阅读源码?一句话,为了写出更好的程序。
一方面,只有了解了代码的执行过程,我们才能更好的使用别人提供的工具和框架,写出高效的程序。另一方面,一些经典的代码背后蕴藏的思想和技巧很值得学习,通过阅读源码,有助于提升自己的能力。
当然,功利的讲,面试都喜欢问源码,阅读源码也有助于提升通过面试的概率。
结合今天的主题,一个很简单的问题,在刚学习集合时,我们都使用过如下代码,但是下面几行代码有区别吗?
List list1 = new ArrayList();
List list2 = new ArrayList(0);
List list4 = new ArrayList(10);
有人可能会说,没指定初始值就按默认值,指定了初始值就按指定的值构造一个数组。真的是这样吗?如果你对上面这个问题有疑惑,就说明你该看看源码了。
学习编程的过程千万不要人云亦云,一定要亲自看看。
如何阅读源码,每个人的方式不同,这里仅以自己习惯的方式来说。以今天的主题为例,ArrayList是干嘛的?怎么用?这就延伸到一条路线,先看类名及其继承体系——它是干嘛的,再看构造函数——如何造一个对象,当然,构造函数会用到一些变量,所以在此之前我们需要先了解下用到的常量值和变量值,最后,我们需要了解常用的方法以及它们是如何实现的。
对于阅读大多数类基本都是按照:类名——>变量——>构造函数——>常用方法。
本文只会选取有代表性的一些内容,不会讲到每一行代码。
类签名
好像没有类签名这个说法,这里是对照函数签名来说的,简单说就是一个类的类名以及它实现了哪些接口,继承了哪些类,以及一些泛型要求。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
从上述代码可以看出,ArrayList实现了:
Cloneable, Serializable接口,具有克隆(注意深度拷贝和浅拷贝的区别)和序列化的能力,
RandomAccess接口,具有随机访问的能力,这里说的随机主要是基于数组实现的根据数组索引获取值,后期结合LinkedList分析更容易理解。
List接口,表明它是一个有序列表(注意,此处的有序指的是存储时的顺序和取出时的顺序是一致的,不是说元素本身的排序),可以存储重复元素。
AbstractList已经实现了List接口,AbstractList中已经实现了一些常见的通用操作,这样在具体的实现类中通过继承大大减少重复代码,需要的时候也可以重写其中方法。
变量
//序列化版本号
private static final long serialVersionUID = 8683452581122892189L;
//常量,默认容量为10
private static final int DEFAULT_CAPACITY = 10;
//常量,初始化一个空的Object类型数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//常量,本质也是一个空的Object类型数组,与EMPTY_ELEMENTDATA用于区别初始化时指定容量0还是默认不指定
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//变量,真正用来存储元素的数组名
transient Object[] elementData;
//数组中实际存储的元素数量,未初始化则默认为0
private int size;
上述变量中的大部分值都比较好理解,令人疑惑的事EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,除了变量名,其他都一样,好在注释和后续的方法为我们说明了,简单说,就是针对初始化时,不同的构造函数选用不同的变量名,即
List list1 = new ArrayList(); //此时用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
List list2 = new ArrayList(0); //此时用EMPTY_ELEMENTDATA
为啥搞这么麻烦,是大神们闲得慌吗?显然不是,不信?请继续往下看。
构造方法
//不指定初始容量的构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_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);
}
}
//通过已有集合直接构造
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
如上所示,ArrayList有三个构造函数:
不指定容量的情况下,此时直接构造一个空的数组,只有当添加第一个元素时,才会扩容为默认容量10。所以说并不是我们经常理解的直接构造一个容量为10的数组,到此时我们才理解为啥很多时候一些规范建议我们指定初始容量,因为这样可以减少一次扩容操作。注意,此时使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 。
指定容量时,小于0抛异常,大于0直接用指定的值构造一个数组,等于0时,也是构造一个空数组,但是此时使用的是EMPTY_ELEMENTDATA。
有啥区别呢?关键在与扩容时的操作。继续往下看。
记住,ArrayList的扩容操作只可能发生在添加元素时。
常用方法
ArrayList的常用方法非常多,这里先排除一大批私有方法和内部类,看一下外部方法(尴尬,差一点一张图截不下):
看起来很多,这里只选取几个常用的,其他的可以类比着看。
add(E e)
第一个最常用的方法,添加元素(add)
public boolean add(E e) {
//检查数组容量是否充足,不够则扩容
ensureCapacityInternal(size + 1);
//注意,下方代码相当于elementData[size] = e; size++;
elementData[size++] = e;
return true;
}
可以看出,在添加元素时,第一步先检查数组容量是否充足,不够的话进行扩容,add方法的关键在于检查容量
检查容量:ensureCapacityInternal(int minCapacity)
//检查容量是否足够,不够则扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//比较实际存储元素+1与数组的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//若构造时不指定容量,则返回默认容量10或者现有实际元素+1中的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//构造时指定了容量,不管是0还是大于0,都返回实际容量+1
return minCapacity;
}
//如果实际容量+1超过了现有容量(数组装不下了),则扩容
private void ensureExplicitCapacity(int minCapacity) {
//记录修改次数,主要是为了遍历元素时发生修改则快速失败,此处不谈。
modCount++;
// 如果现有元素+1大于数组实际长度,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
关键来了,如何扩容
扩容方法:grow(int minCapacity)
private void grow(int minCapacity) {
// 旧容量为数组长度
int oldCapacity = elementData.length;
//新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新容量小于实际元素+1,则按实际元素+1扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//新容量大于数组最大长度,根据实际选择容量为Integer.MAX_VALUE或者MAX_ARRAY_SIZE;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将旧数组元素复制到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
上述代码有一个关键方法Arrays.copyOf(elementData, newCapacity)用来复制集合中的元素,此处不再深入。
回到开始的问题
在创建ArrayList时,
不指定初始容量,即
List list1 = new ArrayList();
//此时,构造一个空的数组,第一次添加元素时,将数组扩容为10,并添加元素。
指定初始容量为0,即
List list2 = new ArrayList(0);
//此时,也构造一个空数组,但变量名和上面不一样。第一次添加元素时,将数组扩容为1,并添加元素。
指定初始容量为10,即
List list4 = new ArrayList(10);
//直接构造一个容量为10的数组,第一次添加元素时,不扩容。
所以说,如果我们大概确定将要使用的元素数量,应当在构造函数中指明,这样可以减少扩容次数,一定程度上提升效率。
小结
到目前为止,只是简单写了下ArrayList的构造函数和add方法,大部分内容都还没有深入。想要把每一个方法都写到,其实很难,也没必要。
通过上面的内容,回顾自己阅读源码的过程,既要“不求甚解”,更要“观其大略”,对于一些核心的过程,我们需要仔细分析;但是对没有经验的新手来说,弄清楚每个细节很难,有些内容现阶段可能还没法理解,把握整体结构很重要,先搞清楚大概,再对每一个细节深入。如果一开始就对某一细节一直深入,很可能迷失其中自己都走不出来了。
看到这里,你问我是不是对ArrayList完全了解了,哈哈,显然没有。但是,写到这里的时候,我的理解又深刻了不少。
心里觉得大概懂了不一定是真的理解,只有抱着把内容写出来让别人看明白的心态,才有可能加深理解。不知,你看明白了没?