大话顺序结构之ArrayList

上篇回顾

上一篇中讲解了线性表的相关概念,通过上一篇文章,希望可以对线性表有一个基本的认识。本篇开始讲述线性表的顺序表是怎么实现的。

先来看一下顺序表的继承关系,这是一个简图。

image.png

1. ArrayList

首先试想一下,我们如此熟悉的ArrayList,如果要你去实现, 你会怎么做?
答案是用数组。那么数组和集合的区别是什么呢?
1) 数组的长度是固定的,我们初始化一个数组,或者指定长度或者指定元素(同样指定了长度)
2) 集合的长度是动态规范的, 我们初始化一个集合,从来不需要处理长度相关的问题
3)数组是没法添加元素的, 集合可以无限制的从尾部添加新元素

其实仔细对比一下,可以发现, 删除一个元素, 修改一个元素, 查询一个元素等功能无论在数组还是ArrayList中实现方式都是一样的,什么?别告诉我在数组里怎么实现你不会。那么就剩下了添加一个元素,从以上的3条对比可以发现, ArrayList与数组的主要区别就在于添加一个元素,如何实现在尾部添加? 如何实现扩容? 其实ArrayList的核心可以讲的东西有两点扩容和移位。

这里我们把查询与修改的实现一笔带过;添加与删除涉及到移位,先讲移位再将删除;添加涉及到扩容,最后讲添加。至于最后的判空啊, 获取长度啊,我相信是不用说的。


ArrayList的实现
 /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
 /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    // Android-note: Also accessed from java.util.Collections
    transient Object[] elementData; // non-private to simplify nested class access

上面提到过, ArrayList的内部数据存储采用数组的形式{$elementData},而当我们初始化一个ArrayList时,我们会得到一个默认长度是10的数组。

ArrayList的查询
/**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
    }

查询的实现从看到源码的瞬间就不用说了,谁不会的话,自己去打屁屁吧。 这里第一步是参数的校验, 这是一个优秀的编码习惯,值得我们去学习。一个全面的,有效的参数校验可以一定程度上保证代码的健康。什么时候需要抛异常,什么时候返回默认值也是个值得推敲的问题。

ArrayList的修改
/**
     * Replaces the element at the specified position in this list with
     * the specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
    }

同查询,普普通通的赋值操作,不值一提。不过可以稍微注意下,返回的值,是未修改之前的原值。

移位

ArrayList是线性表中顺序表的经典结构之一,顺序表的特点是连续的顺序的空间;因此当一个元素添加或删除后,ArrayList会如何做呢?删除之后,对应位置的元素会是空? 当然不是; 一个数组元素已经排位完毕,如何插入一个元素?答案是移位。

1.什么是移位?

当ArrayList插入或移除一个元素时, 指定元素之后的元素会统一向前或向后移动一个位置,称之为移位。那么先官方的解释一下! 假设有一个数组,如下

[0, 1, 2, 3, 4, 5, 6]

对应的位置存储着对应的数字, 此时我想在位置[4]处插入一个元素7,那么插入后的数组是

[0, 1, 2, 3, 7, 4, 5, 6]

我们根据这个现象观察ArrayList的插入操作。首先, 找到位置[4], 然后[4]后面的元素统一后移1位,最后在位置[4]插入指定的元素7。通俗的解释就是像我们日常排队,我们已经排好了一个队伍,这时候领导来了,当然让领导先嘛是不是,这时候我们要所有人往后退一步,腾出第一个位置,领导插进来,皆大欢喜。如图

image.png

再看来下删除操作。如果我想删除元素4,那么删除后的数组是

[0, 1, 2, 3, 5, 6]

我们根据这个现象观察ArrayList的删除操作。首先,找到元素4,位于位置[4],然后删除4, 最后[4]后面的元素统一往前移。还是以排队为例,我们已经排好了一个队伍,忽然队伍中的小王来大姨妈了,要去一趟卫生室。那么后面的人肯定要往前占据小王的位置的,不要说你的生活中不是这样的,你说了我也不信。如图

image.png

通过以上两个例子,我想应该都明白了什么是移位吧。那么我们开始来撸一下代码。

ArrayList的删除
/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];

        /*核心删除操作*/

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

我们直接来看一下核心的删除实现。首先取到待删除值,这个是要返回的; numMoved是什么鬼?看名字就知道了吧,移动的数量,用于判断是否需要执行移位操作;如果没有需要移动的,比如删除最后一个元素,那么很明显就不用移位了嘛,因为移动的位置是从index + 1开始的。

上面提过,数组是长度是固定不变的,如果我想加一个元素到数组,你是怎么做的? 重新创建一个length+1的数组。 这里是通过arraycopy方法来将index(待删除元素的位置) + 1到最后一个元素复制到一个新书组中。
解析下这个API

 public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
src: 现有的数组
srcPos: 从现有数组第几个开始复制
dest: 目标数组,要复制到哪个数组下
destPos: 复制到目标数组的开始位置
length: 复制多少个元素

总结一下以上方法,就是把现在数组从index+1开始到最后一个元素;整体复制到本数组中,复制元素从index开始。不好理解?还是以上面数组为例,原数组是

[0, 1, 2, 3, 4]

我想删除元素2, 那么我就把位置[2]后面的元素[3,4]复制到本数组中,从位置[2]开始放,如下
[0,1,3,4,2]
可以看出3和4从原来的位置[3]与位置[4], 移动到了位置[2]和位置[3]。 也就是把2后面的元素前移了一位。

再看源码的最后一句,最后一个元素置空。size是ArrayList的长度,删除后长度-1。 顺便看一下官方注释, 置空是为了释放最后一个元素的资源,因为最后一个元素不是ArrayList的有效元素了嘛。

好了,删除就说这么多,移位也不再多说了。添加元素是从尾部插入的,因此不需要位移的,前面提过;如果你需要向指定位置添加元素,OK, 还是需要移位的,不过方向与删除相反,也好理解。


ArrayList的添加与扩容

ArrayList的添加是从尾部开始的,因此不需要移位,这也是ArrayList比数组方便的原因之一。前面提到过,数组的长度是固定的,因此在添加元素时,我们要对数组进行扩容。那么扩容的策略如何,接下来看源码

 /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

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

        ensureExplicitCapacity(minCapacity);
    }

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
    }

一看啊呀代码怎么这么多,别慌! 先看add方法,add方法只有两步: 1) 扩容 2) 扩容好了,尾插一个元素 我相信从数组最后插入一个元素不是困难的问题,size++, 扩容之后,数组的长度是+1的,无异议吧,因为插入一个元素。这个size++和之前的--size可是不同的, 前者是后运算, 因此扩容后的数组长度成了size+1,而插入的位置是size, 也就是size++在调用的时候,尚未执行++操作。 而--size正好相反,先运算,再调用。好的,来说下扩容

扩容

从以上添加代码可以看出,最终的扩容策略是在grow()中执行的。

 int oldCapacity = elementData.length;

取出原数组的长度

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

先执行系统的自动扩容, 扩容因子是(oldCapacity >> 1)。 这是一个位移运算,意思是右移1位。在二进制中,右移一位的意思自然是 / 2,因为二进制从左到右的位数越来越低。从右到左分别是2的0次方,2的1次方...那如果左移也就是很明显是x2了。从这句话得出,系统的扩容因子是原数组长度的一半。

 if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

如果系统扩容后,还不足以插入新元素,别忘了ArrayList是有addAll()的, 插入一个军队,这就非同小可了。这时候扩容后的长度就是旧的数组长度 + 新增元素的长度。 再往下一句是一个边界判断,不用解释了吧。

   elementData = Arrays.copyOf(elementData, newCapacity);

到了最后的最后,哎?还是这个套路, 把我现在的数组elementData扩展成为一个长度为newCapacity的数组,我们的扩容就完成了。

面试中可能对扩容策略有提问,所以最好记住这个系统的扩容因子以及最终是如何实现扩容的。ArrayList还有一个add(Element, index)的方法,无非是先实现扩容,然后再进行移位。以上两个核心点搞明白了,这些都不是事儿。

好了,ArrayList篇到这了,是不是很简单呢? 这可是最简单的数据结构了。

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