序文
:
HashMapに関連するビデオや学習資料をいくつか見て、プロジェクトに取り組んでいますが、このナレッジポイントはしばらくの間保留されています。学習するには、自分で行い、自分で書く必要があります。
今日は頭の中で思い出したこと、そしてHashMapに関する知識のポイントとよくある質問を覚えておきます。
これはソースコードを解析しません。ソースコードを確認したい場合は、時間があるときに待って、純粋なコード解釈を行ってください
最初に写真を見てください
備考
:
ノードは赤または黒に分けられます。 ルートノードは黒でなければなりません。 葉ノードはすべて黒でnullです。 赤のノードを接続する2つの子ノードはすべて黒です(赤黒のツリーには隣接する赤のノードはありません)。 任意のノードから開始して、各リーフノードへのパスには同じ数の黒いノードが含まれます。 赤黒ツリーに新しく追加されたノードは赤ノードです。
赤黒木はバランスのとれた二分木であり、自動的にバランスを保つ性質を持つ必要がある上記6つのルールは、赤黒木が自動的にバランスを保つために与えるルールです。
死nも尋ねた
:
HashMap配列にデフォルトの長さがあるのはなぜですか?長さは?
なぜ16ですか?
なぜ書き込みフォーマットは直接16ではなく1 << 4なのですか?
その上限は何ですか?なぜ2はnのべき乗であり、他のものではないのですか?
分析:
Jdk1.8では、HashMapコンストラクタを呼び出してHashMapを定義すると、容量が設定されます。Jdk 1.7では、この操作を実行するには最初のput操作まで待機する必要があり
ます。
デフォルトでは、HashMapの初期化容量を設定すると、実際にはHashMapは値よりも大きい2の1乗を初期化として使用します容量なので、HashMapは渡された値を直接使用する必要はありませんが、計算後に新しい値が取得されます。目的は、ハッシュの効率を向上させることです
如果没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,非常影响性能。
所以为了提升效率,设置默认长度是很有必要的
HashMap中Capacity的值大小为16-------书写:1<<4;
默认值是16,是出于以下几点考虑的:
减少hash碰撞
提高map查询效率
分配过小防止频繁扩容
分配过大浪费资源
在put的过程中,会根据key调用hashcode()方法,计算出相应的Hash值,然后在将得到的int值对
数组长度
进行取模,然而为了考虑性能,Java总采用按位与操作实现取模操作,为了保证能够均匀的使用到每一个位置那么取模后index值的范围必须为0~(2^n)-1,所以当求index的时候必须保证数组长度为2的n次幂,而与Hash值做位运算的值是数组长度-1,保证了其为奇数,从而确保index的范围在
0~(2^n)-1之间,因此保证了能够均匀的使用到每一个位置;如果不能保证为奇数,通过计算得出一个结论,就是在
0~(2^n)-1范围内永远是有一个数无法通过取模运算获得,从而在数组中该值的下标位就会使用不到,就会增加其他位置的Hash碰撞概率,造成其他index的索引位的数据结构复杂化,而影响整个map的查询效率
另外,
数组长度为2的n次幂的时候,不同的key算得的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高
HashMap中Capacity的Max值为1073741824
-------书写:1<<30
接下来就是负载因子
什么是负载因子?
负载因子是和扩容机制有关的,意思是如果当前容器的容量,达到了我们设定的阀值,就要开始执行扩容操作
比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作
理解了负载因子的意思之后我想为什么需要负载因子就不需要解释了
为什么负载因子是0.75,而不是0.5或1?
分析:
HashMap只是一个数据结构,既然是数据结构最主要考虑的就是节省时间和空间,那么怎么做到节省呢?
负载因子1.0
负载因子是1.0的时,也就意味着,只有当数组的16个位置全部填充满了,才会发生扩容。
这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率
负载因子0.5
负载因子是0.5的时,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低,查询效率就会增加。
但是,这时候空间利用率就会大大的降低,原本存储10M的数据,现在就需要20M的空间
总结:
负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率
为什么链表的长度为8时转换为红黑树?为什么为6时又转换链表?
当hashCode离散性很好的时候,树形结构用到的概率非常小,因为数据均匀分布在每个链表中,几乎不会有某链表的长度会达到阈值
在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布
不过理想情况下随机hashCode算法中所有链表中节点的分布频率会遵循泊松分布,也就是链表长度达到8个元素的概率为0.00000006,几乎是不可能事件
所以选择8为链表转换为红黑树的阀值绝非是心血来潮
因为操作红黑树时会涉及到左旋,右旋等操作,而单链表不需要,所以当需要对节点进行操作时,红黑树的成本要高很多,所以为了减小操作成本,当节点数较小时将红黑树转换为链表,再进行操作是较好的选择
HashMap的resize
一、扩容
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容
简单的来说:
HashMap中的元素个数超过数组大小*loadFactor(默认情况下为0.75)时,就会进行数组扩容。
也就是,
默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后通过rehash重新计算每个元素在数组中的位置(
rehash之后数组元素的位置要么在原位置,要么在原位置再移动2次幂的位置
),并修改阀值
备注:
Jdk1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,JDK1.8则不会倒置
Jdk1.8对扩充的优化:
查看原hash值新增的bit是1还是0,是0索引没变,是1索引变成“原索引+oldCap”,从而省去了重新计算hash值的时间,同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket
二、初始化
HashMap会根据
初始容量和负载因子来初始化,当初始容量小于最大条目数除以负载因子时,则会发生 rehash 操作。
rehash操作即重建内部数据结构,一般是增加数组长度为原来的两倍,rehash过程中会重新计算每个元素在数组中的位置,是一个非常消耗性能的操作。所以程序设计时,如果我们已经预知HashMap中元素的个数,那么预设元素的个数是可以有效的提高HashMap性能的
HashMap还有很多值得研究的东西这里就不一一赘述了,喜欢的同学多去看看源码慢慢分析和使用就好了,共勉!加油!