1篇の文章は徹底的にHashMapのHashMapソースコードの解析を読みます(上)

9952 ワード

                 ,HashMap           “  ”,                   HashMap       、       。 
                ,         HashMap       ,          ,               、    HashMap     (         )。     ,             ,               HashMap  ,         HashMap      。               ~

この記事は、HashMapソース分析(JDK 8、分かりやすい)HashMap面接「スター」問題のまとめ、スター問題の答えの順に整理されます.
以下はJDK 8におけるHashMapのソース分析であり、以下のソース分析である.
               

               

               

(1)HashMapのメンバー属性ソース分析
public class HashMap 
   extends AbstractMap
   implements Map,
   Cloneable, Serializable {

    private static final long serialVersionUID
                   = 362498820763181265L;

    //HashMap      16,HashMap 
    //              ,
    //     
    static final int DEFAULT_INITIAL_CAPACITY 
                     = 1 << 4; 

    //HashMap      
    static final int MAXIMUM_CAPACITY
                         = 1 << 30;  
 //       
static final float DEFAULT_LOAD_FACTOR
                     = 0.75f;
//           >=8 ,        
//     ,          >=64,     
static final int TREEIFY_THRESHOLD = 8;

//          <=6 ,         
//    
static final int UNTREEIFY_THRESHOLD = 6;

//     : Node    >=64    ,  
//         >=8,          
//     
static final int MIN_TREEIFY_CAPACITY = 64;
} 

DEFAULT_LOAD_FACTOR:HashMapの負荷係数は、HashMapの性能に影響するパラメータの1つであり、時間と空間のバランスであり、後にHashMapの要素がNode配列に格納されているのが見えます.この配列の大きさはここでは「バケツ」の大きさと呼ばれます.もう1つのパラメータsizeは、HashMapにどれだけの要素をputしたかを指します.size>バケツの数*DEFAULT_LOAD_FACTORの場合、このときHashMapは拡容操作を行います.つまりバケツがいっぱいにならないことです.DEFAULT_LOAD_FACTORはバケツの使用率を測定します.
DEFAULT_LOAD_FACTORが小さい(バケツの利用率が小さい)場合、無駄なスペースが多く(バケツの数DEFAULT_LOAD_FACTOR個のエレメントしか記憶できないので、それを超えると拡張する)、この場合HashMapにputエレメントを入れる際に衝突する確率も小さく、衝突とは、複数のエレメントが同じバケツにputされたこと、衝突が小さいことを意味する(1つのバケツに1つの要素しかないと考えられる)put,getなどのHashMapの操作コストは低く,O(1)と考えられる.DEFAULT_LOAD_FACTORが大きい場合、バケツの利用率が大きい場合(衝突する要素はチェーンテーブルや赤黒い木で接続されているので注意が大きい)、この場合空間利用率が高く、これは1つのバケツに多くの要素が格納されていることを意味し、この場合HashMapのput、getなどの操作コストが比較的大きいことを意味する.各putまたはget操作はチェーンテーブルまたは赤黒ツリーに対する操作となり、コストはO(1)より大きいに違いないので、DEFAULT_LOAD_FACTORは空間と時間の平衡点である.DEFAULT_LOAD_FACTORは小さいので、必要な空間は大きいが、putとgetの代価は小さい.DEFAULT_LOAD_FACTORが大きい場合、必要なスペースは小さいが、putとgetの代価は大きい).拡張操作はバケツの数*2、すなわちNode配列の大きさを拡張前の2倍に調整することであり、なぜ2倍なのか、拡張関数を分析する際に説明するが、これはtrickであり、詳細は後述する.Node配列の各バケツにはNodeチェーンテーブルが格納されており、チェーンテーブルの長さ>=8のとき、Node配列の大きさ>=64のとき、チェーンテーブルは赤黒ツリー構造になります(赤黒ツリーの削除の複雑さはlognであり、チェーンテーブルはnであり、赤黒ツリー構造はチェーンテーブルよりもコストが小さいため).
(2)HashMap内部類——Nodeソース分析
//Node HashMap    
static class Node 
     implements Map.Entry {
        final int hash; 
        final K key;//  map  key
        V value;//  map  value
        Node next;//    
        
        //   
        Node(int hash, K key, V value 
             ,Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

HashMapの内部クラスNode:HashMapのすべてのデータはNode配列に保存されていますが、このNodeはいったい何なのでしょうか.Nodeのhash属性:keyのhashcodeの値を保存する:keyのhashcode^(keyのhashcode>>16).主にhash衝突を減らすためにmapにput(k,v)を押すと、このk,vキー値ペアがノードにカプセル化されます.では、このノードはノード配列のどの位置に置かれますか.index=hash&(n-1)、nはノード配列の長さです.では、なぜhashをこのように計算すると衝突を減らすことができるのでしょうか.hashCode&(n-1)を直接使用してindexを計算する場合、hashCodeの高位ランダム特性はまったく使用されません.nはhashcodeの値に対して小さいため、indexを計算するときは16ビットしか使用できません.これに基づいて、hashcodeの高い16ビットの値を、hashCodeの低い16ビットに異種または混合することによって、hashCodeの低い16ビットのランダム性を増強する.
(3)HashMap hash関数解析
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : 
     (h = key.hashCode()) ^ (h >>> 16);
    }

HashMapはkeyがnull、nullのhashが0(HashMapはkeyがnullのキー値対であることを意味する)を許容し、null以外のkeyのhash高16ビットと低16ビットは、keyのhashCode高16ビットとhashCodeの高16ビット異またはhashCodeの低16ビットからなる.主にhashのランダム性を増強しhash&(n−1)のランダム性を低減し、すなわちhash衝突を低減し、HashMapの性能を向上させるためである.したがって、HashMapのkeyであるhashCode関数の実装はHashMapの性能に大きな影響を及ぼし、極端な場合、すべてのkeyのhashCodeは同じであり、これはHashMapの性能が悪い!
(4)HashMap tableSizeFor関数ソースコード解析
static final int tableSizeFor(int cap) {
    //    :n     1(      ), 
    int n = cap - 1;

    n |= n >>> 1; 
    n |= n >>> 2; 
    n |= n >>> 4; 
    n |= n >>> 8; 
    n |= n >>> 16; 

    return (n < 0) ? 1
         : (n >= MAXIMUM_CAPACITY) 
         ? MAXIMUM_CAPACITY : n + 1;
}

new HashMapの場合、サイズパラメータを入力すると、これはHashMapが入力したHashMap容量をtableSizeFor関数に転送します.この関数の主な機能は、cap以上2の整数乗数の中で最も小さい数を返すことです.すなわち、capに最も近い(>=cap)を返します.2の整数乗の数です.具体的な論理は以下の通りである:1つの数は2の整数次べき乗であり、この数が1を減らすバイナリはマスクの列であり、すなわちバイナリはあるビットから連続する1の列である.したがって、対応するマスクがペアであれば、マスク+1は必ず2の整数次べき乗であり、これもn=cap−1の理由である.例えば、n=000100000_00000000_00000000
n |= n >>> 1;//実行後//n=00011000_00000000_00000000
n |= n >>> 2;//実行後//n=00011110_00000000_00000000
n |= n >>> 4;//実行後//n=00011111_11100000_00000000
n |= n >>> 8;//実行後//n=00011111_11111111_11100000
n |= n >>> 16;//実行後//n=0001111_11111111_11111111
n+1,(n+1)>=cap,2の整数次べき乗を返し,capとの差が最も小さい数である.最後のn+1は必ず2の整数次べき乗であり、必ず>=capである.全体的な考え方は,nのバイナリのk番目が1であれば,上の4つの‘|’演算を経て[0,k]ビットはいずれも1になる,すなわち,一連の連続バイナリ‘1’(マスク),最後にn+1は必ず2の整数次べき乗(オーバーフローしない場合)である.
/   map put (k,v)     Node ,
//   Node    table   
transient Node[] table;

//    keySet values
transient Set> entrySet;

//  map        
    transient int size;

//failFast  ,   ArrayList
// LinkedList        
transient int modCount;

(6)threshold属性分析
int threshold;//       

//    ,    DEFAULT_LOAD_FACTOR
//     ,    0.75
final float loadFactor;

thresholdも重要な属性です.HashMapを作成するとき、この変数の値は、初期容量です.(2の整数次べき乗)後のthresholdの値は、現在のNodetable配列の長さ*loadfactorであるHashMap拡張の閾値である.例えば、HashMapコンストラクタに渡される容量サイズが9の場合、thresholdの初期値は16であり、HashMapの最初の要素に向かうと、内部に長さ16のNode配列が作成され、thresholdの値は160.7に更新される5=12.具体的には、HashMap put要素に向かっているとき、ある要素をputした後、Node配列の要素の個数が13である場合、このとき、拡張(配列内の要素数>threshold、すなわち13>threshold=12)がトリガーされ、具体的な拡張操作は後で詳細に分析され、単純に理解すると、拡張操作はノード配列長2を、元のすべての要素を新しいノード配列に移行する.
(7)HashMapコンストラクタソース分析
//   :  map   , loadfactor
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);
       //  loadfactor
       this.loadFactor = loadFactor;

    /*  ,    tableSizeFor  ,
          :>=initialCapacity、
        2     ,      
                      。
     */
    this.threshold = 
         tableSizeFor(initialCapacity);
}
/*
   HashMap      ,
loadfactor    
    :0.75
   */
public HashMap(int initialCapacity) {
    this(initialCapacity
          , DEFAULT_LOAD_FACTOR);
}

//     ,  loadfactor:0.75,
//      16
public HashMap() {
    this.loadFactor 
          = DEFAULT_LOAD_FACTOR; 
}
//              

コンストラクタから、HashMapは「怠け者ロード」であり、コンストラクタの値には関連する保存された値が保持され、table配列は初期化されていません.mapのputの最初の要素に向かうと、mapは初期化されます.(8)HashMapのget関数ソースコード解析
//  ,     value
public V get(Object key) {
    Node e;
        
    //hash        
    return (e = getNode(hash(key), key))
            == null
            ? null : e.value;
    }

get関数は実質的に、指定したkeyのノードをチェーンテーブルまたは赤黒ツリーで遍歴するプロセスである.また、HashMapのget関数の戻り値は、1つのkeyがmapに含まれているか否かを判断することができず、get戻りnullがそのkeyを含まない可能性があることに注意する必要がある.このkeyに対応するvalueがnullである可能性もあります.HashMapではkeyがnull、valueがnullを許可します.(9)getNode関数ソース分析
//    getNode  
final Node getNode(int hash
                 , Object key) {
    Node[] tab;
    Node first, e;
    int n; K k;
    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)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);
        }
    }
    return null;//   ,  null
}

注意getNodeが返すタイプはNode:戻り値がnullのときmapに対応するkeyがないことを示し、注意区別valueがnull:keyに対応するvalueがnullであればgetNodeに現れる戻り値e.valueがnullであり、このとき戻り値もnullである.つまりHashMapのget関数はmapに対応するkeyがあるか否かを判断できない:get戻り値がnullの場合、そのkeyを含まない可能性があるkeyのvalueがnullである可能性もあります!では、mapにキーが含まれているかどうかをどのように判断しますか?次のcontains関数解析を参照してください.getNode関数詳細解析:(n-1)&hash:現在keyが存在する可能性のあるバケツインデックスであり、put操作時にもindex=(n-1)&hash位置にノードを格納する.getNodeの主な論理:table[index]のノードのkeyが探しているkeyであれば、そのノードに直接戻ります.そうでない場合:table[index]の位置で検索を行うと、ターゲットkeyのノードが存在するか否かを検索する:ここでの検索はまた2種類に分けられる:チェーンテーブル検索と赤黒ツリー検索、具体的な赤黒ツリーの検索は展開されず、赤黒ツリーは弱いバランス(AVLに対する)BSTであり、赤黒ツリー検索、削除、挿入などの操作はO(lon(n))時間の複雑さ内に完了することを保証することができる.赤黒樹の原理は本稿の範囲内ではないが、赤黒樹の各種操作が実現できることを知っておく必要がある.簡単な点では、赤黒樹をBSTと理解することができる.BSTの検索、挿入、削除などの操作の実現は、前の文章にBST java実現説明があり、赤黒樹は実際にはバランスのとれたBSTである.(10)contains関数ソース分析:
public boolean containsKey(Object key) {
    //   get    ,   map put 
    //        Node ,
    //  Node              key
    return getNode(hash(key), key) != null;
}