走进源码——ArrayList阅读笔记

Arraylist

继承自AbstractList,实现了List,RandomAccess,Cloneable,和Serializable接口,具有List的特性,提供可随机访问,提供自身克隆以及序列化的一个容器类。
特点:线程不安全;数组实现

主要成员变量
  • 来自自身的成员变量:
        // 序列化ID
        private static final long serialVersionUID = 8683452581122892189L;
         // 默认初始化容量
        private static final int DEFAULT_CAPACITY = 10;
        // 空的数组
        private static final Object[] EMPTY_ELEMENTDATA = {};
        // 默认容量的空数组
        private static final Object[]  DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        // 真正存放对象的数组
        transient Object[] elementData; 
        // 容器的大小
        private int size;
  • 来自父类和接口的成员变量:
        // 操作计数
        protected transient int modCount = 0;

这些变量都怎么被用到?接下来看方法

方法

方法的话,分几个模块来看吧

  • 初始化
    初始化的构造函数有三个
    第一个,无参构造函数ArrayList(),函数直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

第二个,带int形参的构造函数ArrayList(int initialCapacity),函数首先判断initialCapacity是否为0,如果是0,则将EMPTY_ELEMENTDATA赋值给elementData,如果大于0,就将elementData初始化为一个容量为initialCapacity的对象数组

      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);
        }
      }

第三个,带Collection形参的构造函数ArrayList(Collection<? extends E> c),首先调用CollectiontoArray()方法,返回的结果赋值给elementData,这里值得注意的是,有可能返回的结果不是Object[](有可能是null),所以才会有if (elementData.getClass() != Object[].class)的判断,如果判断成立,调用Arrays.copyOf()来将collection中的数据复制到elementData中。其次,如果collection中没有数据,就将EMPTY_ELEMENTDATA赋值给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;
}
}

这就是全部的构造函数,有疑问,就是三个构造函数都存在赋值一个空数组给elementData,但是为什么无参的赋的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,剩余两个是EMPTY_ELEMENTDATA,带着疑问进入下面的方法

  • 添加
    添加元素这个功能涉及到好几个方法,一个一个来看,首先是最基本的Add方法,把一个元素添加到elementData中,要做这个之前,要扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}

扩容方法,在这里就看到了DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果EMPTY_ELEMENTDATA是由DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值而成,那么就将扩容的数量minCapacityDEFAULT_CAPACITY进行比较,然后取其中的最大值,最大值再传参给ensureExplicitCapacity函数

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

ensureExplicitCapacity(minCapacity)方法,在这个方法中先将操作数记录值modCount自增,然后根据minCapacityelementData.length的大小来决定是否需要增加容量

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

grow(minCapacity)方法,这个就是重头戏

    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);
    }

容量增加为原来的1.5倍的实现

int newCapacity = oldCapacity + (oldCapacity >> 1);

if (newCapacity - minCapacity < 0)的判断是因为oldCapacity有可能为0,这样得到的newCapacity也会为0
if (newCapacity - MAX_ARRAY_SIZE > 0)的判断确保数组的容量不大于Integer的最大值

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :  MAX_ARRAY_SIZE;
    }

最后再返回一个包含原来数据的新容量的数组

elementData = Arrays.copyOf(elementData, newCapacity);

到这里上文的DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA的选择问题也就明了清晰了

  • 调用无参构造函数时创建容器时,当第一次往该容器添加对象时,执行到ensureCapacityInternal,因为这个时候minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity)形参列表中的minCapacity必定是1,所以初始化出来的elementData必定是长度为10的数组,这就是为什么网传默认的ArrayList的容量为10的由来

  • 当调用剩下两个构造函数初始化时,即便初始化出来是容量为0的容器,在第一次往容器里添加对象时,只会让容量扩充到1

// oldCapacity为0,newCapacity为0,minCapacity为1,
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

个人猜测这个设计的由来是为了容器容量的缓慢增长,避免浪费太多的空间,所以以后编码遇到容器类需要存储比较少对象的时候,用带参数的构造函数有利于节省内存

好了,上面就是ArrayList的重头戏扩容,还有剩余的几个add方法也一并过了

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = element;
        size++;
    }

add(int index, E element)方法,根据制定下标开始添加元素,首先会调用rangeCheckForAdd方法判断下标是否有错,然后再判断是否需要扩容,然后调用

System.arraycopy(elementData, index, elementData, index + 1,
size - index);

将index之后的数据往后移一位,最后将element添加到elementDataindex,同时size增加

add系列还有

public boolean addAll(Collection<? extends E> c) // 添加集合
public boolean addAll(int index, Collection<? extends E> c) // 往直接下标开始添加集合

具体实现就不再啰嗦了,大同小异,有兴趣可以自己翻阅,我们进入下一个模块

  • 获取
    ArrayList提供好几种获取容器内对象的方法
第一种,get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}

简单易懂,就不叙述了

第二种,通过Iterator()返回私有内部类Itr对象(Itr实现了Iterator接口),然后在迭代器对象基础上获取容器内的对象

各种迭代器的方法我就不在叙述了

第三种,通过listIterator()或者listIterator(int index)返回私有内部类ListItr对象(ListItr继承Itr并实现了ListIterator接口),然后在迭代器对象基础上获取容器内的对象

ListIterator继承于Iterator,在普通的迭代的基础上增加了往前迭代的操作,因此使用ListIterator迭代ArrayList对象也提供了往前操作对象的方法
这里有一个点注意的是上文中提到的modcount,就拿ListItr中的previous()方法举例吧
我们先过一遍这个方法,游标减1后赋值给i,如果i的值小于0,报错;然后通过闭包获取到外部类的elementData,如果i大于elementData的长度,报错;然后将i赋值给游标,然后直接获取到elementData中的值

@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}

checkForComodification(),就是这个方法用到了modCount我们点进去看下

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

然后再看下代码中expectedModCount是什么东西

private class Itr implements Iterator<E> {
int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
....
}

可以看到expectedModCount就是modCount的一个副本,是在Itr类对象被创建的时候初始化的,也就是一经创建就不会改变。而modCount是在每次操作容器内的数据的时候会自增的,那么什么时候modCount != expectedModCount呢?没错,就是已经通过list对象获取到一个迭代器对象之后,又对list对象进行了数据对象的操作,为此我做了一个测试

意料之中的ConcurrentModificationException

这也是ArrayList线程不安全的一个体现,共享变量是没加锁的,在多线程环境下很容易被修改然后数据就乱套了
其他方法和正常的ListIterator差不多,这里也不再啰嗦了

  • 其他模块
    其他的大概就是set,remove,size,indexOf序列化的两个方法(readObjact(),writeObject()),在搞定了add()的扩容基础上,前四个也是很小case了,而后两个是私有方法,我们是无法直接使用,找了找资料说是调用ObjectOutInputStream序列化的时候会通过反射调用,这里还没了解过和测试过,也不自作高明了。

后言

看了源码才发现源码真的就像高中数理化那些基础一样,基础好了,弄很多东西都顺手拈来,也是看了源码才知道一个类有哪些地方需要注意,甚至是有哪些地方可以优化,而不是一个API工程师

水平有限,难免有错,还请评论区指责下
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容