Java中集合的扩容策略及实现的对比分析

本文详细分析了ArrayList、Vector、HashMap、ArrayMap和SparseArray在Java中的扩容策略。ArrayList和Vector扩容基本相似,HashMap引入红黑树提高了性能,ArrayMap通过优化内存分配和缓存策略减少内存浪费,而SparseArray则针对int key优化。理解这些扩容机制有助于在不同场景下选择合适的数据结构。
摘要由CSDN通过智能技术生成

本文将从源码角度来分析和对比一下集合扩容相关的知识,涉及到的集合框架有:ArrayList,Vector,HashMap,ArrayMap,SparseArray。下面先从ArrayList开始。

  • ArrayList

    ArrayList是以数组实现的一个集合类,在ArrayList的源码中可以看到,所有元素都是被储存在elementData这个全局的数组变量中,而所谓的扩容也是针对这个数组对象进行操作。具体来说,当一个添加元素的动作,即add或addAll被执行时,都会先调用ensureCapacityInternal(…)方法进行容量预检,如果当前elementData数组的容量不足以完成本次添加操作便会进行自动扩容。该方法代码如下:

        //这是一个私有方法,ArrayList提供了另一个public的扩容方法ensureCapacity以满足外界手动扩容的需求
        //其实质也是调用了本方法,在此不做累述
        //minCapacity是指本次添加操作后所需要的数组容量,即elementData.length + newSize
        private void ensureCapacityInternal(int minCapacity) {
                //这个if判断如果成立,则代表当前ArrayList为空,而本次添加操作是第一次添加。
                //此时需要将ArrayList的容量扩充至DEFAULT_CAPACITY=10和本次添加操作所要求的minCapacity二者中的较大者。
                if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
                }
    
                ensureExplicitCapacity(minCapacity);
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            //操作计数器+1
            modCount++;
    
            // overflow-conscious code
            // 确保需要进行扩容,即当前数组的容量小于本次添加操作所要求的新的总容量
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

    在ensureCapacityInternal方法中,一开始会对本次添加操作是否为第一次添加进行判断。从源码:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 中可以得知,DEFAULTCAPACITY_EMPTY_ELEMENTDATA变量指代的是一个空的对象数组,这也是通过无参构造函数new出来的ArrayList对象的初始状态,也即 elementData = {};这种情况下的第一次扩容,如果所要求的新容量小于10,则会直接扩容至10。

    下面继续看到grow方法,这个方法是ArrayList扩容操作的实现

        private void grow(int minCapacity) {
            // overflow-conscious code
            //记录当前elementData数组的容量
            int oldCapacity = elementData.length;
            //新的容量暂定为当前容量的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //如果1.5倍容量仍不满足minCapacity所要求的,则直接将容量定为minCapacity
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            //如果新的容量超过了Integer.MAX_VALUE - 8,则做最大化处理
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            //将当前数组copy到新的数组中
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
        }

    最后的扩容操作,Arrays.copyOf方法会新创建一个大小为newCapacity的数组,然后通过System.arraycopy方法将之前elementData数组中的元素复制到新数组中(起点的index为0),并将新数组返回给变量elementData完成扩容。

  • Vector

    Vector和ArrayList其实身出同门,都是AbstractList的子类并且都实现了List接口,所提供的功能也基本相同,最大的不同在于Vector所有的公有API都是加锁的,也即Vector是线程安全的。但这也正是它不受欢迎的原因之一,太重了。。。言归正传,我们来关心一下Vector的扩容操作。其实基本跟ArrayList相同,方法如下:

        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            //当前容量不够,扩之
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

    可以看到,基本只是方法名不一样而已,再来看看Vector的grow函数,这里还真有些不同:

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
    
            //不同之处在这里,MARK
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

    上面代码块中MARK出的那一句便是不同之处,可以看出Vector的扩容是先判断有没有大于0的capacityIncrement,该变量是通过:

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

    这个带参构造函数传入的,如果不调用这个构造函数,则capacityIncrement为0。在capacityIncrement不为0的情况下,则每次扩容都暂定扩大capacityIncrement个,反之扩容oldCapacity个,即当前容量的一倍。后续的扩容操作和ArrayList一致,也是通过Arrays.copyOf方法来完成,这里不再重复分析。

  • HashMap

    下面来看另一个熟人–HashMap。不同于上面两个List的实现类,HashMap是一个采用哈希表实现的键值对集合,继承自AbstractMap,实现了Map接口并使用拉链法解决Hash冲突,其内部储存的元素并不是在连续内存地址的,并且是无序的。此处我们只关心其扩容操作的逻辑和实现,先说一下,由于要重新创建数组,rehash,重新分配元素位置等,HashMap扩容的开销要比List大很多。下面介绍几个和扩容相关的成员变量:

        //哈希表中的数组,JDK 1.8之前存放各个链表的表头。1.8中由于引入了红黑树,则也有可能存的是树的根
        transient Node<K,V>[] table;
    
        //默认初始容量:16,必须是2的整数次方。这样规定是因为在通过key来确定元素在table中的index时
        //所用的算法为:index = (n - 1) & hash,其中n即为table容量。保证n是2的整数次方就能保证n-1的低位均为1,
        //这样便能保留hash(key)得到的hash值的所有低位,从而保证得到的index在n范围内分布均匀,因为hash算法的结果就是均匀的
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
        //默认加载因子为: 0.75,这是在时间、空间两方面均衡考虑下的结果。
        //这个值过大会导致发生冲突的几率增加,容易形成长链表,降低查找效率;太小则会导致频繁的扩容,降低整体性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值