关于java:阅读源码从ArrayList开始

37次阅读

共计 4761 个字符,预计需要花费 12 分钟才能阅读完成。

前言

为啥要浏览源码?一句话,为了写出更好的程序。
一方面,只有理解了代码的执行过程,咱们能力更好的应用他人提供的工具和框架,写出高效的程序。另一方面,一些经典的代码背地蕴藏的思维和技巧很值得学习,通过浏览源码,有助于晋升本人的能力。当然,功利的讲,面试都喜爱问源码,浏览源码也有助于晋升通过面试的概率。

联合明天的主题,一个很简略的问题,在刚学习汇合时,咱们都应用过如下代码,然而上面几行代码有区别吗?

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<E> 接口,表明它是一个有序列表(留神,此处的有序指的是存储时的程序和取出时的程序是统一的,不是说元素自身的排序),能够存储反复元素。

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_ELEMENTDATADEFAULTCAPACITY_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 齐全理解了,哈哈,显然没有。然而,写到这里的时候,我的了解又粗浅了不少。

心里感觉大略懂了不肯定是真的了解,只有抱着把内容写进去让他人看明确的心态,才有可能加深了解。不知,你看明确了没?

正文完
 0