容器的默认容量以及扩容问题

本文详细介绍了Java中ArrayList和HashMap的默认大小及扩容机制。包括它们的初始容量、加载因子和扩容策略,并深入探讨了HashMap为何选择2的幂作为数组长度的原因。

在面试后台开发的过程中,集合是面试的热话题,不仅要知道各集合的区别用法,还要知道集合的扩容机制,今天我们就来谈下ArrayList 和 HashMap的默认大小以及扩容机制。
在 Java 7 中,查看源码可以知道:ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂,为什么呢???下文有解释)。这就是 Java 7 中 ArrayList 和 HashMap  类 的代码片段:
// from ArrayList.java JDK 1.7private static final int DEFAULT_CAPACITY = 10;
//from HashMap.java JDK 7static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
这里要讨论这些常用的默认初始容量和扩容的原因是:
当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,
这无疑使效率大大降低。加载因子的系数小于等于1,意指即当元素个数超过容量长度*加载因子的系数时,进行扩容。另外,扩容也是有默认的倍数的,不同的容器扩容情况不同。
List 元素是有序的、可重复
ArrayList、Vector默认初始容量为10
Vector:线程安全,但速度慢
    底层数据结构是数组结构
    加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容
    扩容增量:原容量的 1倍
      如 Vector的容量为10,一次扩容后是容量为20
ArrayList:线程不安全,查询速度快
    底层数据结构是数组结构
    扩容增量:原容量的 0.5倍+1
      如 ArrayList的容量为10,一次扩容后是容量为16
 
Set(集) 元素无序的、不可重复。
HashSet:线程不安全,存取速度快
     底层实现是一个HashMap(保存数据),实现Set接口
     默认初始容量为16(为何是16,见下方对HashMap的描述)
     加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
     扩容增量:原容量的 1 倍
      如 HashSet的容量为16,一次扩容后是容量为32
 
Map是一个双列集合
HashMap:默认初始容量为16
     (为何是16:16是2^4,可以提高查询效率,另外,32=16<<1)
     加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
     扩容增量:原容量的 1 倍
      如 HashSet的容量为16,一次扩容后是容量为32
 
接下来我们来谈谈hashMap的数组长度为什么保持2的次幂?
hashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。
这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了
之前已经散列良好的老数组的数据位置重新调换),还有,数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀。

  1. static int indexFor(int h, int length) { 2. return h & (length-1); 3. }
    首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,
    为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 
    看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同
    一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的
    值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组
    长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,
在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

### Java ArrayList 默认容量扩容规则 #### 默认容量 在 JDK 1.8 中,`ArrayList` 的默认初始容量被设置为 `10`。这是通过静态常量 `DEFAULT_CAPACITY` 定义的: ```java private static final int DEFAULT_CAPACITY = 10; ``` 当创建一个无参构造函数实例化 `ArrayList` 对象时,默认会分配一个长度为 `10` 的底层数组来存储元素[^3]。 如果使用带参数的构造函数指定初始容量,则可以自定义该值;如果没有显式初始化任何容量,那么第一次调用 `add()` 方法时才会真正创建底层数组并赋予其默认大小[^1]。 #### 扩容机制 对于 `ArrayList` 来说,在向其中添加新元素之前,它总是先检查当前内部数组是否有足够的剩余空间容纳即将加入的新项。这是因为虽然表面上看似乎无需关心具体尺寸问题(因为它是动态调整型容器),但实际上为了提高性能和减少频繁重新分配内存资源带来的开销,每次增加操作都需要确保有足够的连续区域可供写入数据。 以下是具体的扩容逻辑描述: - **首次扩充**: 如果初次尝试扩展而现有结构不足以满足需求,并且请求新增数量小于等于十,则直接设定目标总规模达到十个单位以上再执行复制迁移过程[^4]; - **后续多次扩增情况下的处理原则如下所示**: 当再次遇到超出界限的情形发生之后,程序会选择把原有效载荷乘以因子三除二即扩大百分之五十作为新的预期边界条件进行试探性验证是否合适;假如此种方式仍然无法解决短缺状况的话,则精确计算确切所需额外补充部分的数量进而完成最终定稿工作流程[^2]^. 这种策略既兼顾了一般情况下较为常见的少量追加情形下效率上的考量,同时也保留了应对极端大规模突发增长场景的能力保障不会轻易触发OutOfMemory错误风险的同时维持较好的运行表现水平. ```java // 示例代码展示如何手动模拟ArrayList的部分行为特征 public class CustomList { private Object[] elementData; private int size; public CustomList() { this.elementData = new Object[10]; // 初始容量设为10 } public void add(Object obj) { ensureCapacityInternal(size + 1); elementData[size++] = obj; } private void ensureCapacityInternal(int minCapacity) { if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 增大到原来的1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } } ``` 上述实现片段简单模仿了标准库中的某些核心特性以便于理解整个运作原理图景.
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值