今、それらをインタビューこれらのメーカーは、基本的にはHashMapについての質問のいくつかを聞いてきますが、また、このコレクションの開発にもしばしば使用されています。だから私はこの記事を書くために調査・分析に多くの時間を費やしています。この記事は、分析にjdk1.8に基づいています。長い、しかし緩やかです。私はあなたが我慢して、収穫を読むと信じています。
問題の分析と、まず、
この記事では、次のような問題を解決することを望んで。
基礎となるデータ構造は何である(1)HashMapのはありますか?
原則の下を達成するためのCRUD操作でどのような(2)HashMapの?
どのように(3)HashMapの拡大を達成するには?
(4)HashMapのハッシュの競合はどのように解決されますか?
(7)HashMapのはなぜスレッドセーフではありませんか?
ここでは、HashMapのベールを明らかに、これらの質問を取ります。
第二に、理解のHashMap
jdk1.7はあまり変わっていないまで、第1 JDK1.2でのHashMapが現れ始めます。しかし、jdk1.8は、突然、多くの変更を行いました。最も注目すべき変更点の一つは、次のとおりです。
ストレージ構造の前にjdk1.8リスト配列+ +赤黒木になるために、アレイjdk1.7 +鎖です。
保証は、データの整合性がない場合に加えて、ハッシュマップは、複数のスレッドCRUD操作で、その特定の要素一方のHashMap、スレッドセーフではありません。
ここでは、開始段階の分析によってステップ。
第三に、綿密な分析のHashMap
図1に示すように、基礎となるデータ構造
比較分析のために、我々は最初の図のjdk1.7メモリ構造を与えます。
我々は、アレイ内の各要素のために、それは、後に格納されたデータ要素の増加内部配列の最初の要素であり、それはリストた、jdk1.7に、図から見ることができています要素を格納するためのリンクリストがあるかもしれません。これが有名な「ジッパースタイル」保存方法です。
そう、だから、数年後に、より多くの記憶素子、要素を見つけるために時間内に、長く、長くなっリストだけではなく(リストが追加と削除に適して見ていない)の効率を向上させることができなかったのではなく、多くのことをドロップこのリスト上の記事では改善しました。どのようにそれを改善するには?それが探してツリーにリストを置くことです、はい、それは赤、黒の木です。したがって、これはなると次のHashMapのデータ構造。
私たちは、最適化が赤黒ツリーにリスト構造の一部であることがわかります。元jdk1.7の利点は、jdk1.8時間のため、高効率の追加と削除で追加および削除するだけでなく、高効率、だけでなく、検索効率を向上させます。
注:それは確かに向上する赤黒木効率になって、8以上のリストの長さ、配列の長さは64以上である場合にのみ、リストは赤黒木に変換されるわけではありません、
質問:赤、黒の木は何ですか?
赤黒木は、効率を探し、検索効率が赤黒木が非常に高い、自己均衡二分探索木であるOのリスト(N)からO(LOGN)に還元されます。あなたは赤黒木を理解していない場合、それは問題ではありません、あなたは赤黒木の検索効率がOKで非常に高いです覚えています。
質問2:なぜ突然リスト全体の全てが赤、黒の木にはないのですか?
問題を意味することはこれです、私たちは時間が、リストの長さに等しくなるまで待たなければならない理由があることは8以上である、赤、黒の木に形質転換しましたか?ここでは、2つの方法で解釈することができます
(1)リストの複雑な構造より赤黒木構造は、リストのノードにあまり必ずしもアレイ+リストの構造性能よりも高くないかもしれない配列+ +赤黒木構造リストの全体的なパフォーマンスから思われる場合。やり過ぎの意味のように。
(2)HashMapの頻繁な拡張を、分割し、非常に時間がかかる赤黒木の底の再構築を引き起こすし続けます。したがって、リストの長さは、赤黒木に長い時間であり、大幅に効率が向上します。
OK、ここに私たちは、基礎となるデータ構造は、ハッシュマップの理解を持っていると信じています。今、上記のチャートでは、我々はどのように記憶素子を見てください。
2、記憶素子を置きます
以下のほとんどがこのように使用するとき私たちは、記憶素子です。
public class Test {
public static void main(String[] args) {
HashMap<String, Integer> map= new HashMap<>();
//存储一个元素
map.put("张三", 20);
}
}
复制代码
ここでのHashMap <文字列、整数>は、最初のパラメータは、二番目のパラメータの値は、一緒になって、キーと値のペアを結合と呼ばれています。putメソッドを呼び出すために必要なだけのストレージ。その基礎となる原理はどのようにそれのようなものでしょうか?ここに与えられた最初のフローチャートです。
あなたが見ることができる場合は、このプロセスの上に、私たちはこのプロセスを整理するために言葉を使用し、赤の3は、その決意ブロック、ターニングポイントを書いて、知りません。
(1)第一段階:キーと値のペアを渡す置くメソッド呼び出し
(2)ステップ2:ハッシュ値のハッシュアルゴリズムを使用し
(3)ステップ3:他のキーの衝突の位置か否かをハッシュ値に格納されている位置を決定し、決定し
(4)第4ステップ:競合がない場合、直接アレイに記憶することができます
(5)第5ステップ:競合する場合は、だけでなく、今回どのようなデータ構造で決定しますか?
(6)第6工程:この場合、データ構造は、赤黒木に直接次いで赤黒木を、ある場合
(7)第7ステップ:データ構造は、リンクされたリストである場合、挿入がより大きくなった後、この時、それはかどうか等しい8に決定されます
(8)第8ステップ:8より大きい挿入した後、赤黒木を調整する必要がある、挿入
(9)ステップ9:8より大きくないの挿入後、それはリンクされたリストの末尾に直接挿入することができます。
上記のデータは、単にプロセスを見て、全体のプロセスに挿入されている十分ではありません、我々はまた、コードはこのプロセスに従って書かれているかを確認するために、深いソースコードの下に移動する必要があります。
上記のputメソッドでマウスの焦点は、F3をクリックして、我々はソースコードの中に入れることができるようになります。見てみましょう:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
复制代码
言い換えれば、メソッド呼び出しが実際にputVal方法で入れました。putVal方法は、5つのパラメータを取ります。
(1)第1のパラメータのハッシュ:ハッシュのハッシュ値を計算するためのメソッドを呼び出します
(2)第2のパラメータキー:つまり、我々は、ジョン・ドウの場合でキー値を渡し
(3)第3のパラメータ値:我々は値を渡す値、20の場合であります
(4)第四のパラメータonlyIfAbsent:ときに、同じキー値であり、変更することなく、既存の
(5)第五のパラメータを追い出し:falseの場合、アレイ内のモードを作成され、それは一般的に真です。
これら5つのパラメータの意味を知って、私たちはこのputVal方法に入ります。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一部分
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//第二部分
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//第三部分
else {
Node<K,V> e; K k;
//第三部分第一小节
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第三部分第二小节
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第三部分第三小节
else {
for (int binCount = 0; ; ++binCount) {
//第三小节第一段
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//第三小节第一段
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//第三小节第三段
p = e;
}
}
//第三部分第四小节
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//第四部分
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
复制代码
一見すると、このコードは、実際の吐き気と嘔吐する場合を参照するには、初めて、上の読みする欲求は絶対にありませんが、分析の到来と一緒にフローチャートを描くようになった、我々はそれがはるかに良くなると信じています。私たちは、(4つの部分に分かれ、全体としての)コードを分割します:
(1)タブアレイにおけるノード<K、V> []タブが示されています。ノード<K、V> pは、Pノードが現在挿入されて表します
(2)パートI:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
复制代码
この部分は、配列が空の場合は平均値を表し、その後、リサイズ方法を使用して新しい配列を作成します。拡張が挙げられますとき、ここでサイズ変更方法は、次のセクションでは、出て話すことはありません。
(3)第2部:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
复制代码
&ハッシュ - iは、アレイ内の挿入位置を示し、(1 N)として算出されます。newNode直接競合しない場合、競合の挿入位置は、配列に挿入することができるか否かを判断する必要があり、このフローチャートは、最初のブロックが決定さに相当します。
あなたは競合のハッシュ値を挿入した場合、それは紛争を扱う、第三部に行くだろう
(4)第三部分:
else {
Node<K,V> e; K k;
//第三部分a
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第三部分b
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第三部分c
else {
for (int binCount = 0; ; ++binCount) {
//第三小节第一段
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//第三小节第一段
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//第三小节第三段
p = e;
}
}
//第三部分d
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
复制代码
私たちは、競合がトラブルに実際にあることがわかりますが、幸い、私たちはこの部門の一部でした
a)第一のセクションの第3部分:
if (p.hash == hash
&&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
复制代码
ここで決意テーブル[i]は重要な要素は、それが直接に同一のP値に挿入されているかのように挿入された電子の古い値を置き換えるあります。
b)第二のセクションの第3の部分:
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
复制代码
挿入赤黒木データ構造を解析することは、赤黒木に直接あるputTreeVal、場合赤黒木を表し、リンクされたリストですか。このフローチャートと決意に対応する第2のフレームの内部。
C)第3のセクションの第3の部分:
//第三部分c
else {
for (int binCount = 0; ; ++binCount) {
//第三小节第一段
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//第三小节第一段
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//第三小节第三段
p = e;
}
}
复制代码
データ構造は、リンクされたリストである場合は直接newNode(ハッシュ、キー、値、NULL)が存在しない場合、配列は、最初のトラバースの表は、存在しなければなりません。新しい値がある場合は、直接、古いものを交換してください。
なお:要素が存在し、binCount> = TREEIFY_THRESHOLDを判断した場合、リストの末尾に挿入されている - 1。リストの現在の長さを分析する8の閾値よりも大きい場合、それは赤黒木に現在のリストは、treeifyBinであろう場合よりも大きいです。これはまた、第三であり、フローチャートは判定ブロックに対応します。
(5)パートIV:
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
复制代码
成功した挿入後、だけでなく、実際には、閾値閾値よりも大きいキーのサイズの数に存在するかを決定します。拡大が始まったよりも大きい場合。
3、拡張
なぜ拡張それは?それは、あまりにも多くの要素を入れて、電流容量が十分でないことは明らかです。この目的のために、我々は最初の分析のために再度、フロー図を与えます。
この拡張は比較的簡単で、新しいハッシュテーブルの容量と新しい容量のしきい値を計算する最初のものですHaspMapの拡大は、その後、新しいハッシュテーブル、新しいハッシュテーブルを再マップする古いキーを開始します。それは古いハッシュテーブルに赤黒木に来る場合、新しいハッシュテーブルにマップされたものスプリット赤黒木に関するものです。私たちの通常の拡張プロセスの容量を持つプロセス全体のラインは、我々は結合フローチャートのコードを分析しました。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//第一部分:扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//第二部分:设置阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//第三部分:旧数据保存在新数组里面
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//只有一个节点,通过索引位置直接映射
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,需要进行树拆分然后映射
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//如果是多个节点的链表,将原链表拆分为两个链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//链表1存于原索引
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//链表2存于原索引加上原hash桶长度的偏移量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
このコードは嫌なのと同じ量であるが、我々はまだ分析するためにセグメント化:
(1)パートI:
//第一部分:扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
复制代码
コードはまたNengkanmingbaiある:アレイの容量が最大値を超えた場合まず、その後、閾値は、整数の最大値に直接設定され、超えていない場合、その後、ここで留意すべきオリジナルの2倍の膨張は、oldThr << 1であります達成するための操作をシフト。
(2)パートII:
//第二部分:设置阈值
else if (oldThr > 0) //阈值已经初始化了,就直接使用
newCap = oldThr;
else { // 没有初始化阈值那就初始化一个默认的容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//为当前的容量阈值赋值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
复制代码
第一の閾値が初期化されている場合、その後、直接古いしきい値を使用することを意味している場合、他のファースト。そして、第二には、他に表して初期化されていない場合、それは新しいアレイ容量と新しいしきい値を初期化します。
(3)第三の部分
第三部はまた、非常に複雑で、内部の新しい配列に古いデータをコピーすることです。次のような状況があることがあります注意してください。
:膨張後、ハッシュ値が新しいビット= 0を計算する際に関与する場合、元の位置に次に拡張要素=位置
B:拡張した後、もしコンピューティング= 1に関与する新たなビットのハッシュ値を、古い拡張後の拡張要素位置=位置+元の位置。
新規事業の何ビットのハッシュ値は、それに関与していますか?我々は、2進数に値をハッシュビット・コンピューティングは、第五の逆数である参加加えます。
そして、ハッシュテーブルの2倍の長さの拡張後に、ハッシュテーブルは、その後、低および高に分け、半分ずつに分かれて、非常に良いデザインがある場合は、キーと値のペアの元一覧表示することができ、下半分に裁判官にe.hash&oldCap == 0で、この裁判官はそれの利点は何高で半分が、ありますか?
たとえば、N = 16、10000バイナリは、ビット5が1であり、e.hash&oldCapは、0.5ビットに等しいe.hash放電の50%の確率に相当する0または1であり、依存新しいハッシュ・テーブル内のロー、ハイ新しいハッシュテーブルの上に50%の確率。
OK、このステップは基本的にでも展開のこの部分を終了し、ストレージの原則も理解してどのくらいのストレージ拡張の要素、自分自身を明らかにしたという、問題が解決されていないがあり、アドレスの競合は、拡張後に出現しましたどのようにそれを行うには?
図4に示すように、アドレスの競合の解決
前提アドレス競合の解決が繰り返されたハッシュ値を計算することである、我々はHashMapのを見て、それがハッシュ値を計算する方法です。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
コードは実際に、なぜXOR演算それを使用するためのハッシュコード16 XORを通じて、ハッシュ値は、超簡単ですか!あなたが理解する絵を描きます:
すなわち、XOR演算により、衝突しにくい、ハッシュ均一から計算することができるされています。しかし、それを解決するためにどのようにこの時間、競合現象がありますどうなりますか?
データ構造の方法は、我々は一般的に使用される競合のハッシュを扱う:開発方法に対処して、チェーンアドレス、法律、公共オーバーフロー領域の確立をハッシュします。メソッドのハッシュマップの競合は、ハッシュチェーンアドレス法を扱っています。
このアプローチの基本的な考え方は、ハッシュアドレスiのすべての要素が同義語鎖、単一のリストの先頭ポインタとハッシュテーブルのi番目のユニットの存在と呼ばれる単一のリンクされたリストで構成され、したがって、検索挿入および削除することです主にシノニム・チェーンインチ 頻繁に挿入および欠失の場合にも適用チェーンアドレス法則。
私は、一つ一つが、OKにチェーンに配置されたとき、私たちはすべての接続速度が遅い、アドレスの競合が発生すると考えています。ただ、HashMapの基礎となるデータ構造をエコーします。
5、設定のHashMap
上記の問題が発生する可能性があり、我々はすでに彼のコンストラクタが長年の懸案だっについて、説明してきました。のは、彼の良いコンストラクタについてお話しましょう:
彼は4つの工法の合計です。
最初の1:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
复制代码
第二:
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
复制代码
第三:
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
复制代码
第四:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
复制代码
明らかに、これらの4つのコンストラクタ第四ほとんどのトラブルは、我々は第四コンストラクタを分析する必要があり、他の3人は自然に理解するだろう。loadFactorとInitialCapacityの値:上記の2つの新しい用語があります。私たちは一つ一つ分析します:
(1)InitialCapacityの値初期容量
公式には、例えば、2,4,8,16のために、2のべき乗のN倍の値を入力するために私達に尋ねたので、これらは、私たちは気をつけていない突然のは、20-どのように行うに入りますか?それは、あなたが入力した値に基づいて仮想マシンを物質の電源からのN 2の最近の20倍の値を探しません、彼に最も近い16は、彼が初期容量のための16を取ったと言います。
(2)loadFactor負荷率
荷重係数は、デフォルト値は0.75です。initailCapacity * loadFactor = HashMapの容量:ハッシュテーブルスペースを使用して度合いを示す負荷率は、そのような式があります。したがって、この場合、インデックスは効率を低下させるので、より多くの要素、構成要素より、大きなリストを収容することが可能であるハッシュテーブルの大きな負荷率を充填度が高いです。逆に、小さな負荷率が悪いスペース料金が発生します。この時点で、よりスパースのリストのデータ量が、今回のインデックスの高効率です。
デフォルト値は0.75なぜだろうのですか?私たちは、JDKのドキュメントの傍受します:
英語は、私は本当に無知な力を見て人々を見て良いではありませんが、意味についての良いニュースは、理解することができます。Poisson_distributionは、ポアソン分布のものということではありません三行目を見てください。しかし、キーがあります
バケットが素子8に到達すると、確率は、負荷率として0.75、すなわち、非常に小さくなっており、8以下の各鎖の衝突位置の長さは、ほとんど不可能です。バケットが素子8に到達すると、確率は、負荷率として0.75、すなわち、非常に小さくなっており、8以下の各鎖の衝突位置の長さは、ほとんど不可能です。
6、HashMapのはなぜスレッドセーフではありませんか?
この問題を解決したい、答えはメソッド内のソースコードは、すべての非スレッドセーフな方法であるため、このようなキーワードが同期見つからない、シンプルです。我々は、スレッドの安全性を保証することはできません。だから、そこのConcurrentHashMap。
私はあなたへの書き込みは、最終的にはコアコンテンツの一部を終了します。もちろん、これらの質問の表面の多くが含まHashMapのは、網羅することはできません。省略した場合、我々は将来的に補完します。ようこそ批判。