前言

为啥要阅读源码?一句话,为了写出更好的程序。

一方面,只有了解了代码的执行过程,我们才能更好的使用别人提供的工具和框架,写出高效的程序。另一方面,一些经典的代码背后蕴藏的思想和技巧很值得学习,通过阅读源码,有助于提升自己的能力。当然,功利的讲,面试都喜欢问源码,阅读源码也有助于提升通过面试的概率。

结合今天的主题,一个很简单的问题,在刚学习集合时,我们都使用过如下代码,但是下面几行代码有区别吗?

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完全了解了,哈哈,显然没有。但是,写到这里的时候,我的理解又深刻了不少。

心里觉得大概懂了不一定是真的理解,只有抱着把内容写出来让别人看明白的心态,才有可能加深理解。不知,你看明白了没?

阅读源码,从ArrayList开始的更多相关文章

  1. 浅析Java源码之ArrayList

    面试题经常会问到LinkedList与ArrayList的区别,与其背网上的废话,不如直接撸源码! 文章源码来源于JRE1.8,java.util.ArrayList 既然是浅析,就主要针对该数据结构 ...

  2. JDK1.8源码分析02之阅读源码顺序

    序言:阅读JDK源码应该从何开始,有计划,有步骤的深入学习呢? 下面就分享一篇比较好的学习源码顺序的文章,给了我们再阅读源码时,一个指导性的标志,而不会迷失方向. 很多java开发的小伙伴都会阅读jd ...

  3. Java源码-集合-ArrayList

    基于JDK1.8.0_191 介绍   在Java中,对于数据的保存和使用有多种方式,主要的目的是以更少的资源消耗解决更多的问题,数组就是其中的一种,它的特点是所有的数据都保存在内存的一段连续空间中, ...

  4. 【转】使用 vim + ctags + cscope + taglist 阅读源码

    原文网址:http://my.oschina.net/u/554995/blog/59927 最近,准备跟学长一起往 linux kernel 的门里瞧瞧里面的世界,虽然我们知道门就在那,但我们还得找 ...

  5. Spring源码解析——如何阅读源码(转)

    最近没什么实质性的工作,正好有点时间,就想学学别人的代码.也看过一点源码,算是有了点阅读的经验,于是下定决心看下spring这种大型的项目的源码,学学它的设计思想. 手码不易,转载请注明:xingoo ...

  6. Spring源码解析——如何阅读源码

    最近没什么实质性的工作,正好有点时间,就想学学别人的代码.也看过一点源码,算是有了点阅读的经验,于是下定决心看下spring这种大型的项目的源码,学学它的设计思想. 手码不易,转载请注明:xingoo ...

  7. 阅读源码(III)

    往期系列: <由阅读源码想到> <由阅读源码想到 | 下篇> Medium上有一篇文章Why You Don't Deserve That Dream Developer Jo ...

  8. 阅读源码(IV)

    往期系列: <由阅读源码想到> <由阅读源码想到 | 下篇> <阅读源码(III)> Eric S.Raymond的写于2014年的<How to learn ...

  9. How Tomcat works — 一、怎样阅读源码

    在编程的道路上,通过阅读优秀的代码来提升自己是很好的办法.一直想阅读一些开源项目,可是没有合适的机会开始.最近做项目的时候用到了shiro,需要做集群的session共享,经过查找发现tomcat的s ...

随机推荐

  1. 【学习中】Unity<中级篇> Schedule

    章节 内容 签到 Unity3D 实战技术第二版视频教程(中级篇) 1.游戏引擎发展史 2.Unity发展史 3.3D图形学与必要组件 5月19日 4.核心类_GameObject类 5月19日 5. ...

  2. FZU - 2038 -E - Another Postman Problem (思维+递归+回溯)

    Chinese Postman Problem is a very famous hard problem in graph theory. The problem is to find a shor ...

  3. openresty(nginx+lua)初识

    1.新增项目配置文件: vim /usr/example/example1.conf --将以下内容加入example1.conf server { listen 80; server_name _; ...

  4. Spring MVC实例创建(一)

    Spring MVC Spring MVC 为展现底层提供的基于MVC设计理念的优秀的Web框架,是目前最流行的MVC框架之一.Spring3.0后全面超越Struts2,成为最为优秀的MVC框架.S ...

  5. shell知识点:${} 的神奇用法

    为了完整起见,我这里再用一些例子加以说明 ${ } 的一些特异功能:假设我们定义了一个变量为:file=/dir1/dir2/dir3/my.file.txt我们可以用 ${ } 分别替换获得不同的值 ...

  6. Java 后端开发常用的 10 种第三方服务

    请肆无忌惮地点赞吧,微信搜索[沉默王二]关注这个在九朝古都洛阳苟且偷生的程序员.本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题. 严格意义上 ...

  7. range如何倒序

    for j in range(3,-2,-1): 表示对3进行每次加-1的操作,直到-2,但不包括-2 print(j) 打印出3 2 1 0 -1都换行展示的

  8. Mysql数据分片技术(一)——初识表分区

    1. 为什么需要数据分片技术 2. 3种数据分片方式简述 3. 分片技术原理概述 4. 对单表分区的时机 1为什么需要数据分片技术 数据库产品的市场 在互联网行业内,绝大部分开发人员都会遇到数据表的性 ...

  9. netty字符串流分包

    @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Obj ...

  10. 【域控日志分析篇】CVE-2020-1472-微软NetLogon权限提升-执行Exp后域控日志分析与事件ID抓取

    前言:漏洞复现篇见:https://www.cnblogs.com/huaflwr/p/13697044.html 本文承接上一篇,简单过滤NetLogon漏洞被利用后,域控上的安全及系统日志上可能需 ...