Javaソースコード分析容器シリーズ-HashMap

HashMapの実装Mapインタフェース。HashMapのは、非常に広く使用されているが、それはスレッドセーフではありません、複数のスレッドでは、あなたが(複数のスレッドがConcurrentHashMapのを推奨)必要な追加の同期メカニズムを提供する必要があります使用している場合。

HashMapのクラス図は比較的簡単ですが、主に継承されたクラスAbstractMap、もう一つ注意すべきは、しかし実装していないIterableインターフェイスを、しかし、HashMapの自体が、イテレータの機能を達成しました。

JDK1.8に基づいて、

メンバ変数と定数

HashMapのは、Node[]各インデックスが呼び出され、配列バケット

各キーと値のペアが使用されているNode店舗には、これは、単独でリンクされたリストデータ構造です。各バケットは、リストにリンクキーと値のペアを複数に格納されてもよいです。

定数

次のようにその意義に使用HashMapの定数:

// 初始容量(桶的个数) 2^4 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
// 最大容量(桶的个数) 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的装载因子(load factor),除非特殊原因,否则不建议修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 单个桶上的元素个数大于这个值从链表转成树(树化操作)
static final int TREEIFY_THRESHOLD = 8;
// 单个桶上元素少于这个值从树转成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 只有桶的个数大于这个值时,树化操作才会真正执行
static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

メンバ変数

以下で使用HashMapのメンバ変数:

// HashMap 中的 table,也就是桶
transient Node<K,V>[] table;
// 缓存所有的键值对 
transient Set<Map.Entry<K,V>> entrySet;
// 键值对的个数
transient int size;
// HashMap 被修改的次数,用于 fail-fast 检查
transient int modCount;
// 进行 resize 操作的临界值,threshold = capacity * loadFactor
int threshold;
// 装载因子
final float loadFactor;
复制代码

ノードテーブルは、アレイであるlength典型的に2 ^ n個、しかし、0であってもよいです。

初期化

実際にはHashMapの初期化は、2つのだけのことを行って:

  • threadholdの値を決定します
  • 値は、決定loadFactorです

ユーザは、初期容量と負荷係数を通過することができます。HashMapの容量は常に2 ^ n個パラメータが渡されていない場合、2 ^ n個に変換されます2 ^ n個

// HashMap.tableSizeFor()
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
复制代码

Integer.numberOfLeadingZeros()戻り値INT(32)のバイナリ表現の最後の非ゼロ数字の前のゼロの数。たとえば、2:

0000 0000 0000 0000 0000 0000 0000 010
复制代码

従ってInteger.numberOfLeadingZeros(3)は、30を返し。

バイナリで1として表されます:

1111 1111 1111 1111 1111 1111 1111 1111
复制代码

>>> 符号なし右シフト、右シフト30 -1得られます。

0000 0000 0000 0000 0000 0000 0000 011
复制代码

3を取得します。

だから、後に-1 >>> Integer.numberOfLeadingZeros(cap - 1)値が返されなければならない2 ^ n-1の最後の戻り値がでなければならないので、2 ^ n個、興味がテストに行くことができます。

HashMapの初期化時間は、また、マップオブジェクト、現在のコンテナに入ってくる地図オブジェクト要素を受け入れることができます。

最初のキーと値のペアに挿入されたときに、着信地図オブジェクトの方法の例に加えて、実際に遅延初期化する方法であるバケットアレイを作成するつもりはないが、呼び出しresize()バレルを初期化する方法。

詳細な見てみましょうresize()操作を。

膨張機構

ArrayListの異なる、手動膨張処理なしのハッシュマップ、現在の状況に応じてのみ、自動膨張容器。

展開によって運営resize()達成方法、主に操作するの拡大は、3つのことを乾燥さ:

  • 樽の数を決定
  • の値の閾値を決定
  • 新しいバケットにすべての要素

パラメータ説明

  • oldCap:拡張前のバケット数
  • oldThr:拡張前のしきい値
  • newCap:拡張後のバケット数
  • newThr:拡張しきい値の後

次のように拡張プロセスは、次のとおりです。

新しいノード(バレル)アレイの拡張、キー操作の再ハッシュのため、元の容器、および、新しいバケツに作成されます。

HashMapのは、容量限界があり2 ^ {30}、それが1073741824であり、バレルの数がこの数を超えない、最大閾値は2147483647であり、1未満の二倍の最大容量です。

この設定は、バケットの数が最大容量に達する示す場合、拡張は動作しないであろう。

実現

図HashMapの構成上述したように、各バケットは、キーの同一のハッシュ値(ハッシュ衝突)のために、それは同じバケットに配置されます、最初のノードのリストです。これは、HashMapの解決ハッシュ衝突と呼ばれているジッパーの法則JDK1.8後、キーと値のペアの挿入、使用時の補間の端部を、補間ではなくヘッド。

HashMapのやHashtableの機能大幅に一致しています。キーと値のHashMapのは、nullにすることができます。ここでは、キー値がするかどうかの比較のヌル主流の地図です。

地図 キーがnullにできるかどうか 値はnullにできるかどうか
HashMapの それはあります それはあります
ハッシュ表 ノー ノー
ConcurrentHashMapの ノー ノー
TreeMapの ノー それはあります

HashMapのは、スレッドセーフではありません。マルチスレッド環境では、そのような使用などの追加の同期メカニズムを使用する必要性Map m = Collections.synchronizedMap(new HashMap(...));

HashMapのもフェイルファストメカニズムをサポートしています。

ハッシュ法

HashMapのは、直接パフォーマンスに影響を与えるため、ハッシュ法は非常に重要です。キー挿入位置は、ハッシュ法によって決定されます。仮定するハッシュ法は、基本的な動作として、槽内の要素の均一な分布を可能にgetし、put操作が操作時定数です(O(1))。

ハッシュ方式は、二つの特徴を必要とします。

  • 計算の結果は、ランダムに十分に必要
  • 量があまり大きくない計算

具体的には以下のように実装されたHashMap:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码

>>>符号なし右シフト演算である、我々は上に述べました。3373707は、バイナリに変換されます私のコンピュータ値に基づいて計算キー「名前」は、そこにされていると仮定します。

0000 0000 0011 0011 0111 1010 1000 1011
复制代码

16の後に右:

0000 0000 0000 0000 0000 0000 0011 0011
复制代码

そして、排他的論理和演算:

0000 0000 0011 0011 0111 1010 1011 1000
复制代码

それはNでなければならないので、最終的に、この値のハッシュマップの長さから1を引いて、操作を取る2 ^ X全てバイナリの1の(N-1)は、次の操作に対応するいくつかのハッシュ値をとります。

index = (n - 1) & hash
复制代码

インデックスが挿入位置への鍵です。

ハッシュ()関数が実際に呼び出さランダム十分のキー位置、挿入するために使用された摂動関数を特定の戦略に興味があれば、これを参照することができます記事

注:Object.hashcode()、オブジェクトがメモリアドレスを返すネイティブメソッドです。クラスがequalsメソッドを変更する場合は、デフォルトの比較のメモリアドレスのはObject.equals()メソッドは、その後、hashCodeメソッドはまた、一貫して対等とhascodeの振る舞いをするように変更する必要があります。キーと値のペアの結果を見ての過程で等号があるでしょうかどうかは、ペアを見つけることができないので、ハッシュコードは、同じではありませんが、本当です。

容量と負荷係数

:HashMapを使用している場合、2つのパラメータは、パフォーマンスに影響がある初期容量負荷係数を

HashMapの容量は浴槽の数で、初期容量は、浴槽の初期作成されたインスタンスの数です。

負荷率の展開を決定するために使用される時間を、拡張のための操作は、バレルの数は、元の設定されます、コンテナ内のすべての要素が再割り当てされる場所を、優れた拡張費用は、拡張操作可能な限り低減されるべきです。

負荷率のデフォルト値はトレードオフであり、0.75 -time性能およびスペースオーバーヘッド値。大きな負荷率は、スペースのコストが低減され、設定されているが、運用などのパフォーマンスの外観は低下し、逆も同様です。

初期容量と負荷係数の値は、注意深く拡張動作可能な限り低減するために測定されなければならないHashMapの初期化では、特別な状況の場合、デフォルトパラメータを使用することができます。

容器(バレルの数)と要素を通過する時間が必要な数に比例したHashMapの容量。反復時のパフォーマンスが重要な場合は、送信しない初期容量、あまりにも設定し、またすべき負荷率が提供されて小さいです。

ツリーの操作

具体的な方法を説明する前に、重要な内部動作をHashMapの知っている必要があります:ツリーを

HashMapのは、紛争を解決するために、ハッシュジッパー法を用いました。それらが接続されている一緒のリストに基づいて、に割り当てられていた同じバケットに対する複数のキー。しかし、それはリストが長すぎる場合は、その後、多くの操作のHashMapのは、維持することができなくなり、問題に直面するだろうO(1)動作時間を。

極端な場合には、バケット内のすべてのキーと値のペア。そして、両方の取得、削除などの操作の時間計算O(N)HashMapのソリューションを使用することである赤黒木の代わりに、リンクリスト、赤黒木でクエリ安定の時間の複雑さをO(LOGN)

浴槽は、ツリーリスト(のように変わります後ろのHashMap単一のバケット内の要素の数は、浴槽64(MIN_TREEIFY_CAPACITY)の数よりも8(TREEIFY_THRESHOLD)以上を超える場合TreeMap)、この動作は、ツリー操作と呼ばれます。

以上のタブ8の単一の要素よりも、しかしバレルの数が64未満である場合、ツリー注動作を行わないが、あろう、ということ拡張操作として次の

// HashMap.treeifyBin() method
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // other code...
}
复制代码

プロセスのツリーは、すべてのノードがツリーノードは、組成物(具体的には赤黒木構築プロセスがこの赤黒木を見ることができる置換されているリストにある記事)。そして、各ノード間の相対的な関係の間に、ツリーのリストに順番にノードを介して、変更されません。nextこの関係変数を維持します。

ノードツリーは、ツリーが6未満(UNTREEIFY_THRESHOLD)である場合、それはツリー構造リストに再変換されます。リンクされたリストに、次のリストを再構成することにより、ツリーノードの各ノード:

// HashMap.ubtreeify()
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
            tl = p;
    }
    return hd;
}
复制代码

極端な状況(バレル内のすべてのキーと値のペア)の顔には、ツリーの動作はしません縮退あまりのHashMapの性能を保証します。

CRUD操作

方法を取得する:実用的な方法は、getNodeが達成getメソッドを使用することです。

// HashMap.getNode()
final Node<K,V> getNode(int hash, Object key) {
    // 首先检查容器是否为 null 以及 key 在容器中是否存在
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 找到相应的桶,从第一个节点开始查找,如果第一个节点不是要找的,后续节点就分成链表或者红黑树进行查找
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果链表已经转成了红黑树,则在红黑树中查找
            if (first instanceof TreeNode)
               return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 如果不是树,则在链表中查找
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   return e;
            } while ((e = e.next) != null);
        }
    }
}
复制代码

置く方法:キーと値のペアが実際に使用される挿入または更新するHashMap.putVal()方法。あなたは、キーと値のペアを挿入して初めて、それが引き金となり、拡張操作を。

// HashMap.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);
                   // 如果链表的长度大于等于 8,就会尝试进行树化操作
                   if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                        break;
                }
                // 如果找到了 key,则跳出循环
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
                p = e;
            }
        }
        // 如果 key 已经存在,则把 value 更新为新的 value
        if (e != null) { 
           V oldValue = e.value;
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
            return oldValue;
        }
    }
    // fail-fast 版本号更新
    ++modCount;
    // 如果容器中元素的数量大于扩容临界值,则进行扩容
    if (++size > threshold)
        resize();
    return null;
}
复制代码

達成するために同様の方法を削除し、メソッドを取得します。

明確な方法は、すべてのバケットがペアをクリアするためにnullに設定されているマップされます。

その他の操作が完了するまでにいくつかの基本的な操作の組み合わせです。

JDK8の新機能

JDK8では、地図は、いくつかの新しいメソッドを追加して、これらのメソッドのHashMapのは、フェイルファストメカニズムのサポートを追加するために書き直されました。

これらの方法は、CRUDが達成上記の方法を使用します。

値が存在しないgetOrDefault方法、デフォルト値を返します:

HashMap map = new HashMap<>();
map.put("name", "xiaomi");

map.getOrDefault("gender","genderNotExist"); // genderNotExist
复制代码

マップのキーと値のペアを横断するforeach方法は、ラムダ式を受け取ることができます。

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

map.forEach((k, v) -> System.out.println(k +":"+ v));
复制代码

キーで挿入されたキーが存在しない場合にのみputIfAbsent方法:

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

map.putIfAbsent("gender", "man");
复制代码

computeIfAbsent方法は、その後、いくつかの後処理の鍵値によって地図を挿入し、キーが存在しない場合のように、いくつかの操作を簡単にするために、以下の方法1および2の機能を使用します。

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
if (age == null) {
    age = 18;
    map.put("key", age);
}
// 方法2:
map.computeIfAbsent("age",  k -> {return 18;});
复制代码

computeIfPresent方法は、キーの存在下にあるキーと値のペアの処理、次いで全く同じ機能のためのマップは、以下の方法1及び2を更新します。

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
Integer age = 18 + 4;
map.put("key", age);

// 方法2:
map.computeIfPresent("age", (k,v) -> {return 18 + 4;});
复制代码

同じキー値をマージするために使用される方法は、以下の方法1と一致する特徴2組み合わされます。

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
age += 14;
map.put("key", age);

// 方法2:
map.merge("age", 18, (oldVal, newVal) -> {return (Integer)oldVal + (Integer)newVal;});
复制代码

その他の機能

HashMapのも反復機能を実現し、HashMapのイテレータの3つの特定の実現があります。

  • KeyIterator:キーのトラバースマップ
  • ValueIterator:値のトラバースマップ
  • EntryIterator:マップのキーと値を横断しながら、

しかし、3つのイテレータは、直接、間接的HashMapのメソッドを呼び出すことによって得られません。

  • 取得及び使用のHashMap.keySet()メソッドによってKeyIterator
  • HashMap.vlauesによってValueIterator()を取得および使用する方法
  • 取得及び使用のTreeMap.entrySet()メソッドによってEntryIterator

各キー、値、およびキー+値の同様の実装Spliteratorイテレータは、Spliteratorを達成しました。

オリジナル

関連記事

いいえマイクロチャネル注目しない、話に何か他のもの

おすすめ

転載: juejin.im/post/5e06b72ee51d455846232c93