HashMap

HashMap是Java中基于哈希表的数据结构,其默认初始化容量为16,负载因子为0.75。当链表长度超过8,或者数组容量大于64时,HashMap会将链表转换为红黑树以优化查询效率。负载因子选择0.75是为了平衡空间和时间效率。扩容时,HashMap会创建新的2倍容量的数组并重新哈希所有元素。文章还探讨了为何使用2的幂次幂作为容量以及扩容时为何采用位移运算而非乘法。
摘要由CSDN通过智能技术生成

比较常见Map家族

数组的特点:查询效率高,插入,删除效率低。

链表的特点:查询效率低,插入,删除效率高。

java1.7 之前是数组+链表 ,1.8 之后是 数组+链表+红黑树

HashMap是基于哈希表的Map接口的非同步实现。

/**
 * The default initial capacity - MUST be a power of two.
 * 默认初始化容量,必须是2的次方
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 1*2*2*2*2 = 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 * 最大容量。即HashMap的数组容量必须小于等于 1 << 30
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

默认初始化容量。这个是在采用无参构造方法实例化HashMap的时候,默认使用16作为初始化容量。在第一次put的时候使用16来创建数组。当超出阈值(默认是0.75*16=12),数组容量为扩容为之前的2倍。

/**
 * The load factor used when none specified in constructor.
 * 默认的负载因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认负载因子;默认负载因子值为0.75,但是可以通过构造方法来指定。一般是不建议修改的。

// 树形化阈值
static final int TREEIFY_THRESHOLD = 8;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
// 树形化的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;

树形化阈值;即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为o(n) , 红黑树查询的时间复杂度为 o(log n )

红黑树与链表转换条件

链表升级成红黑树的条件

① 链表的长度大于8

② HashMap数组的容量大于等于64

红黑树退化成链表的条件

扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表。

删除元素 remove( ) 时,在 removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 根的左子树的左子树为空,都会发生红黑树退化成链表。

Size和Capacity的区别

为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?答案有两种:

阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。这种情况就是牺牲了空间的利用率来保证时间。

当负载因子是1.0的时候,意味着当数组中的元素满了才开始扩容,会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

为什么使用2的幂方?

(n-1)&hash中n就是hash表原来的长度,n-1就会使二进制发生如下的变化:

(n-1)&hash,接下来就会与被插入的对象hash进行按位与运算,看下面几个例子:

发现待插入的元素与n-1后的数值进行&运算后得到的还是它原来的数值。

如果数组扩容不是按2的n次幂来运算,那么就会有hash冲突的情况出现。比如数组长度16进行一次扩容以后变成了25(二进制11001),与带插入新元素进行&运算时就会出现hash冲突,如下:

可以看出当扩容不是2的n次幂也就是不是原来长度的2倍时,长度为25的数组与不同的待插入元素进行&运算得出了同样的结果,发生了hash冲突。但数组扩容是2的n次幂时就可以尽量避免hash冲突的发生。

为啥HashMap中初始化大小为什么是16呢?

首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:int index =key.hashCode()&(length-1);

hahmap每次扩容都是以 2的整数次幂进行扩容

因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。

那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?

答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小。

问题又来了,为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?

这个问题就比较简单了,因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

HashMap为什么需要扩容?扩容机制是什么?

HashMap的默认容量是16。当HashMap中的元素越来越多的时候(int index =key.hashCode()&(length-1);),碰撞的几率也就越来越高(因为数组的长度是固定的)。

HashMap的扩容公式:initailCapacity * loadFactor = 扩容的阈值

比如:HashMap默认的数组容量是16,LoadFactor是0.75, 那么当HashMap中元素个数超过16*0.75=12的时候久会触发扩容,调用rehash方法将数组容量扩大2倍,扩展为2*16=32。然后重新计算每个链表、红黑树里的元素在数组中的位置,并且再放进数组里(resize()),而这是一个非常消耗性能的操作。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

HashMap扩容分为两步:

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。

  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

为什么要重新Hash呢,不直接复制过去呢?因为长度扩大以后,Hash的规则也随之改变。

Hash的公式---> index = HashCode(Key) & (Length - 1)

原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了,之前的所有数据的hash值得到的位置都需要变化。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值