ArrayList的扩容机制和核心底层数据结构

ArrayList

底层数据结构

transient Object[] elementData; // 用于存储数据的buffer数组

ArrayList集合的底层是由一个可变的动态 Object[]数组组成的,由于他是由数组构成的,所以该集合类框架对于随机访问的效率很高,时间复杂度为 O(1)

    public boolean add(E e) {
        modCount++;// 结构更改次数
        add(e, elementData, size);// 套娃
        return true;
    }
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();// 自动扩容
        elementData[s] = e;// 在数组的末尾插入元素
        size = s + 1;
    }

但是在插入元素的时候,如果采用默认的 add(E e)便会在数组的末端插入,时间复杂度近似为 O(1)

    public void add(int index, E element) {
        rangeCheckForAdd(index);// index校验
        modCount++;// 结构更改次数
        final int s;
        Object[] elementData;
//        当size 与 数组的容量一致,执行自动扩容
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();// 扩容
        System.arraycopy(elementData, index,
                elementData, index + 1,
                s - index);// 数组元素往后移动一位,腾出桶位给待插元素
        elementData[index] = element;
        size = s + 1;
    }

如果在数组中间插入一个元素,那么他会先将该位置后面的元素整体往后移动一位后,再把待插入元素插入,这时的时间复杂度就近似为 O(n)

线程不安全

纵观 ArrayLis的源码,所有的方法均没有进行线程安全方面的设计,故可见他是一个线程不安全的集合,与之相对应的就是 Vector,该集合框架,他就是一个线程安全的,对其内部每一个方法均进行线程安全的控制。根据以上的一些信息我们可以得出:在单线程的情况下,我们使用 ArrayList的效率会比 Vector高很多,但是在多线程的情况下,使用 ArrayList会有线程不安全的风险。

扩容机制

ArrayList中,其扩容的方法主要为 grow()方法:

我们进入内部:

private Object[] grow() {
    return grow(size + 1);
}

我们发现他其实是一个套娃的方法:

private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
            newCapacity(minCapacity));
}

其实到这里我们即可看出一点猫腻了, 用于储存数据的数组会被复制给另外一个数组,并把另一个数组的引用赋值给 elementData,而新数组的大小就是扩容后 elementData数组的大小,这个大小由方法 :newCapacity(int minCapacity)确定。

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);// 1.5 倍扩容
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow,该值过大,超过了 int 的表示范围,为负数
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0) //当newCapacity>MAX_ARRAY_SIZE时,返回 hugeCapacity
            ? newCapacity
            : hugeCapacity(minCapacity);
}

看到这里我们神秘的扩容机制将不再神秘了,细细分析,其实核心的还是最后一个 return语句:

return (newCapacity -  MAX_ARRAY_SIZE <= 0) //当newCapacity>MAX_ARRAY_SIZE时,返回 hugeCapacity
            ? newCapacity
            : hugeCapacity(minCapacity);

现在,newCapacity = 1.5*oldCapacity,它和定义的最大的数组大小 MAX_ARRAY_SIZE进行比较,如果小于,则返回 newCapacity进行扩容,否则调用 hugeCapacity(int minCapacity)

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

很容易看出:当给定的最小容量 minCapacity比最大的数组大小大的时候,返回 int所能表达的最大整数 Integer.MAX_VALUE,否则,返回最大数组大小 MAX_ARRAY_SIZE。到此, ArrayList的扩容机制,基本就搞清楚了。

我们来总结一下:

变量说明:

newCapacity:新计算的扩容大小

minCapacity:最小的所需数组大小

MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 :最大的数组大小(不同的 JVM在数组的处理上,可能会有头字符,所以预留了8个位置)

针对 ArrayList而言,会在往集合中添加元素的时候进入到扩容入口函数 grow(),当当前集合中的元素数量与存储数组的长度一样的时候,会触发扩容机制,会把当前数组的长度 + 1 作为扩容判断的标准,并计算出新的大小 newCapacity(约为当前数组长度的1.5倍),主要分为以下几种情况:

  • newCapacity <= minCapacity,判断当前的数组是否为默认的空数组,如果是,就返回默认初始化大小值(10)与 minCapacity中大的作为实际的扩容大小;否则直接返回 minCapacity
  • 如果 newCapacity不超过最大数组大小 MAX_ARRAY_SIZE,直接返回 newCapacity作为扩容大小,否则,用 minCapacityMAX_ARRAY_SIZE比较,如果不超过MAX_ARRAY_SIZE,则返回MAX_ARRAY_SIZE作为扩容大小;否则返回 Integer.MAX_VALUE作为扩容大小。

总之,在一般的情况下,基本都是以近乎于原数组大小的1.5倍扩容。

补充:ArrayList的数组是在第一次添加元素的时候将数组设置为默认的初始化大小的,是一种懒加载方式。

Vector

底层数据结构和线程安全

protected Object[] elementData;

ArrayList的底层数据结构一样,都是利用一个可变大小的动态 Objec[]数组来存储数据结构。他和 ArrayList有着类似的性质,这里不再赘述,但是和 ArrayList不同的是:他是线程安全的,可以在多线程的条件下,安全地访问一个 Vector对象 ,不过他的访问效率比 ArrayList低;他也不是和 ArrayList类似的懒加载方式:

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

他提供的构造函数,都是套娃上面这个方法,在构造对象的时候就已经给数组申请到了一定的空间。无参构造时,默认大小也是 10,同时还可以提供自定义的扩容增加数量 capacityIncrement

扩容机制

由于他和 ArrayList的机制类似,其他的代码,这里不再展示,只关心关键方法 newCapacity(int minCapacity)

   private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 在原来的数组大小的基础上加上扩容量,默认值为0
        // 故在没有给定自定义扩容量或者扩容量给定为0时,
        // 默认扩容到原来的 2 倍。
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

总体来看,扩容机制和 ArrayList类似,只是新的大小计算方式不同。

LinkedList

底层数据结构、线程不安全

见名知义,是采用的 链表这种数据结构,来作为存储数据的数据结构。

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList的底层实际上是采用的双向链表。(JDK1.6之前是循环链表,在 JDK1.7取消了)。

为啥要采用双向链表?

因为在链表这种数据结构中查询一个元素,必须从头结点开始一次遍历,并且他在内存空间中是一种内存地址不连续的存储方式,寻址代价比较大,如果链表比较长,那么查询效率就会比较低,采用双向链表,有两个首位节点,,可以从两个方向开始遍历链表,在查询遍历的时候,一定程度上可以提高一定的效率。举个源码中的例子:

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    Node<E> node(int index) {
        // assert isElementIndex(index);
		// 分段查找
        if (index < (size >> 1)) {// 在链表的前半部分,从头结点开始遍历
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {// 在链表的后半部分,从尾节点开始遍历
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

另外,和 ArrayList一样,是一种线程不安全的集合,如果进行了线程同步,那么该结构的访问效率将进一步降低。

欢迎关注个人公众号

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值