HashMapインタビューの必需品

HashMapの概要

HashMapはMapインターフェースの実装です。HashMapは空のキーと値のキーと値のペアを許可します。HashMapはHashtableの拡張バージョンです。HashMapはスレッドセーフではないコンテナです。スレッドセーフなマップを作成する場合は、次のことを検討してください。 ConcurrentHashMapを使用します。HashMapは、内部に格納されているキーと値のペアの順序を保証できないため、HashMapは順序付けされていません。

HashMapの基礎となるデータ構造は、配列+リンクリストのコレクションです。配列はHashMapでも呼び出され桶(bucket)ます。HashMapをトラバースするために必要な時間損失は、HashMapインスタンスバケットの数+(Key-Valueマッピング)の数です。

HashMapには、初期容量と負荷係数の2つの重要な要素があります。初期容量は、ハッシュテーブルバケットの数を指します。負荷係数は、ハッシュテーブルの充填度の尺度です。十分な数のエントリがある場合。ハッシュテーブル、負荷率と現在の容量を超えると、ハッシュテーブルが再ハッシュされ、内部データ構造が再構築されます。

HashMapはスレッドセーフではないことに注意してください。複数のスレッドが同時にHashMapに影響を与え、少なくとも1つのスレッドがHashMapの構造を変更する場合は、HashMapを同期する必要があります。Collections.synchronizedMap(new HashMap) スレッドセーフなマップを作成するために使用できます 

HashMapは、イテレータ自体の削除に加えて、外部のremoveメソッドがフェイルファストメカニズムを引き起こす可能性があるため、イテレータ独自のremoveメソッドを使用してみてください。イテレータの作成中にマップの構造が変更された場合、ConcurrentModificationException 例外がスローされ ます。

重要な属性

初期容量(16)

HashMapのデフォルトの初期容量は、DEFAULT_INITIAL_CAPACITY 属性によって管理され ます。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

デフォルトの負荷率

static final float DEFAULT_LOAD_FACTOR = 0.75f;

拡張メカニズムの原則は、HashMap> HashMap容量*負荷係数に格納されている量が、HashMapの容量が2倍になることです。

変更

HashMapでは、modCount 変更の数を示すために使用さ れます。これは主に、HashMapの同時変更のフェイルファストメカニズムに使用されます。

拡張しきい値

HashMapでは、 threshold 拡張を表すしきい値、つまり初期容量*負荷係数の値を使用します。

負荷率

loadFactor HashMapの密度を表す負荷係数を表します。

HashMapデータ構造

JDK1.7では、HashMapは配列+リンクリストの実装を採用しています。つまり、リンクリストは競合の処理に使用され、同じハッシュ値を持つリンクリストは配列に格納されます。ただし、配列内に要素が多い場合、つまりハッシュ値が等しい要素が多い場合、キー値で順次検索する効率は低くなります。

そのため、JDK 1.7と比較して、JDK 1.8は基礎となる構造にいくつかの最適化を行っています。各配列の要素が8より大きい場合、赤黒木に変換されます。目的は、クエリの効率を最適化することです。

ノードインターフェース

ノードノードは、Map.Entryインターフェイスを実装するHashMapのインスタンスを格納するために使用され ます。最初に、マップ内の内部インターフェイスエントリインターフェイスの定義を見てみましょう。

Map.Entry

// 一个map 的entry 链,这个Map.entrySet()方法返回一个集合的视图,包含类中的元素,// 这个唯一的方式是从集合的视图进行迭代,获取一个map的entry链。这些Map.Entry链只在// 迭代期间有效。interface Entry<K,V> {
   
     K getKey();  V getValue();  V setValue(V value);  boolean equals(Object o);  int hashCode();}

ノードノードは、ハッシュ値、キー、値、および次のノードノードへの参照の4つの属性を格納します

 // hash值final int hash;// 键final K key;// 值V value;// 指向下一个Node节点的Node类型Node<K,V> next;

Map.Entryはエントリチェーンによって接続されているため、ノードノードもエントリチェーンです。新しいHashMapインスタンスを構築するとき、これらの4つの属性値は着信に分割されます

Node(int hash, K key, V value, Node<K,V> next) {
   
     this.hash = hash;  this.key = key;  this.value = value;  this.next = next;}

KeySet内部クラス

keySetクラスは、AbstractSet抽象クラスを継承keyset() します。HashMapのメソッドを使用してKeySetインスタンスを作成し、HashMapの キーキーを操作するように設計されています。

// 返回一个set视图,这个视图中包含了map中的key。public Set<K> keySet() {
   
     // // keySet 指向的是 AbstractMap 中的 keyset  Set<K> ks = keySet;  if (ks == null) {
   
       // 如果 ks 为空,就创建一个 KeySet 对象    // 并对 ks 赋值。    ks = new KeySet();    keySet = ks;  }  return ks;}

値内部クラス

Valuesクラスの作成は実際にはKeySetクラスと非常に似ていますが、KeySetはマップkey-value 内のキーを操作するように設計されており、Valuesはキーと値のペアの値を使用するように設計されています

public Collection<V> values() {
   
     // values 其实是 AbstractMap 中的 values  Collection<V> vs = values;  if (vs == null) {
   
       vs = new Values();    values = vs;  }  return vs;}

 

EntrySet内部クラス

key-value 動作内部クラス上 のキーと値のペア

// 返回一个 set 视图,此视图包含了 map 中的key-value 键值对public Set<Map.Entry<K,V>> entrySet() {
   
     Set<Map.Entry<K,V>> es;  return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;}

HashMapプットプロセス

まず、ハッシュメソッドを使用してオブジェクトのハッシュコードを計算し、ハッシュコードに従って配列内の場所を決定します。配列内にノードノードがない場合は、直接入力します。対応する配列にすでにノードノード、リンクリストの長さが分析されます。長さが8より大きいかどうかを判断するために、リンクリストの長さが8未満の場合、JDK1.7より前のヘッド補間法が使用され、テールが使用されます。補間方法はJDK1.8以降で変更されます。リンクリストの長さが8より大きい場合、ツリー化が実行され、リンクリストは赤黒木に変換されて赤黒木に格納されます。

//JDK1.8final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {
   
     Node<K,V>[] tab; Node<K,V> p; int n, i;  // 如果table 为null 或者没有为 table 分配内存,就resize一次  if ((tab = table) == null || (n = tab.length) == 0)    n = (tab = resize()).length;  // 指定hash值节点为空则直接插入,这个(n - 1) & hash才是表中真正的哈希  if ((p = tab[i = (n - 1) & hash]) == null)    tab[i] = newNode(hash, key, value, null);  // 如果不为空  else {
   
       Node<K,V> e; K k;    // 计算表中的这个真正的哈希值与要插入的key.hash相比    if (p.hash == hash &&        ((k = p.key) == key || (key != null && key.equals(k))))      e = p;    // 若不同的话,并且当前节点已经在 TreeNode 上了    else if (p instanceof TreeNode)      // 采用红黑树存储方式      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);    // key.hash 不同并且也不再 TreeNode 上,在链表上找到 p.next==null    else {
   
         for (int binCount = 0; ; ++binCount) {
   
           if ((e = p.next) == null) {
   
             // 在表尾插入          p.next = newNode(hash, key, value, null);          // 新增节点后如果节点个数到达阈值,则进入 treeifyBin() 进行再次判断          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st            treeifyBin(tab, hash);          break;        }        // 如果找到了同 hash、key 的节点,那么直接退出循环        if (e.hash == hash &&            ((k = e.key) == key || (key != null && key.equals(k))))          break;        // 更新 p 指向下一节点        p = e;      }    }    // map中含有旧值,返回旧值    if (e != null) { // existing mapping for key      V oldValue = e.value;      if (!onlyIfAbsent || oldValue == null)        e.value = value;      afterNodeAccess(e);      return oldValue;    }  }  // map调整次数 + 1  ++modCount;  // 键值对的数量达到阈值,需要扩容  if (++size > threshold)    resize();  afterNodeInsertion(evict);  return null;}

HashMapをトラバースする方法

HashMapトラバーサルの基本クラスは次のとおりです HashIterator。これはHashイテレータであり、HashMap内の抽象クラスであり、その構造は比較的単純です。hasNext、remove、nextNodeメソッドの3つのメソッドしかなく、nextNodeメソッドは3つのイテレータで構成されています。実現された、これらの3つのイテレータは

  • KeyIterator 、キーをトラバースします

  • ValueIterator、値をトラバースします

  • EntryIterator、エントリーチェーンをトラバースする

それらのトラバース順序は同じであり、使用HashIterator 中のnextNode メソッドですべてトラバースさ ます 

final class KeyIterator extends HashIterator        implements Iterator<K> {
   
           public final K next() { return nextNode().key; }    }final class ValueIterator extends HashIterator  implements Iterator<V> {
   
     public final V next() { return nextNode().value; }}final class EntryIterator extends HashIterator  implements Iterator<Map.Entry<K,V>> {
   
     public final Map.Entry<K,V> next() { return nextNode(); }}

HashIteratorでのトラバーサル

abstract class HashIterator {
   
     Node<K,V> next;        // 下一个 entry 节点  Node<K,V> current;     // 当前 entry 节点  int expectedModCount;  // fail-fast 的判断标识  int index;             // 当前槽  HashIterator() {
   
       expectedModCount = modCount;    Node<K,V>[] t = table;    current = next = null;    index = 0;    if (t != null && size > 0) { // advance to first entry      do {} while (index < t.length && (next = t[index++]) == null);    }  }  public final boolean hasNext() {
   
       return next != null;  }  final Node<K,V> nextNode() {
   
       Node<K,V>[] t;    Node<K,V> e = next;    if (modCount != expectedModCount)      throw new ConcurrentModificationException();    if (e == null)      throw new NoSuchElementException();    if ((next = (current = e).next) == null && (t = table) != null) {
   
         do {} while (index < t.length && (next = t[index++]) == null);    }    return e;  }  public final void remove() {...}}

Nextとcurrentは、それぞれ次のノードノードと現在のノードノードを表します。HashIteratorは初期化中にすべてのノードをトラバースします

HashMapスレッドは安全ではありません

HashMapはスレッドセーフなコンテナーではなく、その不安定さは、複数のスレッドによるHashMapの同時書き込み操作に反映されます。スレッドAとBが2つある場合、最初のAはキーと値のペアをHashMapに挿入する必要があります。バケットの位置が決定されて配置されると、Aのタイムスライスが使い果たされます。Bが実行する番です。次に、Bが実行されます。Bがキーと値のペアを正常に挿入することを除いて、Aと同じ操作です。AとBの挿入位置(バケット)が同じである場合、スレッドAは実行を継続した後にBのレコードを上書きし、データの不整合を引き起こします。もう1つのポイントは、HashMapが展開しているときに、resizeメソッドがループを形成し、無限ループを引き起こし、CPUを急上昇させることです。

HashMapがハッシュ衝突を処理する方法

HashMapの最下層は、ビットバケット+リンクリストを使用して実装されます。ビットバケットは要素の挿入位置を決定します。ビットバケットはハッシュメソッドによって決定されます。複数の要素のハッシュを計算して同じハッシュ値を取得する場合、 HashMapは、複数のノード要素を結合します。すべてが対応するビットバケットに配置され、リンクリストを形成します。ハッシュ衝突を処理するこの方法は、チェーンアドレス法と呼ばれます。

ハッシュ衝突を処理する他の方法には、オープンアドレス方式、再ハッシュ方式、およびパブリックオーバーフロー領域の確立が含まれます。

HashMapはどのように要素を取得しますか

まず、テーブル内の要素が空であるかどうかを確認し、次にハッシュに基づいて指定されたキーの場所を計算します。次に、リンクリストの最初の要素が空かどうか、空でない場合は一致するかどうか、一致する場合はこのレコードを直接返します。一致しない場合は、次の要素の値がnullかどうかを判断します。空のTreeNode 場合は直接戻ります空でない場合インスタンスかどうかを判断し 、TreeNodeインスタンスの場合はTreeNode.getTreeNode 抽出した要素を直接使用します 。それ以外の場合は、次の要素がnull位置になるまでループを実行します。

HashMapとHashTableの違いは何ですか

同じ点

HashMapとHashTableはどちらも、ハッシュテーブルに基づいて実装されます。その中の各要素は、 key-value キーと値のペアです。HashMapとHashTableは、Map、Cloneable、Serializableインターフェースを実装します。

  • 親クラスは異なります AbstractMap 。HashMapはDictionary クラスを継承しますが、HashTableはクラスを継承します 

  • ヌル値は異なります:HashMapは空のキーと値の値を許可しますが、HashTableは空のキーと値の値を許可しません。HashMapは、Nullキーを通常のキーとして扱います。nullキーの重複は許可されていません。 

     

  • スレッドセーフ:HashMapはスレッドセーフではありません。追加や削除など、複数の外部操作がHashMapのデータ構造を同時に変更する場合は、同期操作を実行する必要があります。キーまたは値の変更のみが変更操作ではありません。データ構造。Collections.synchronizedMap またはyes などのスレッドセーフなマップを作成することを選択できます ConcurrentHashMapHashTable自体はスレッドセーフです。

  • パフォーマンス:HashMapとHashTableはどちらも単一リンクリストに基づいていますが、HashMapはputまたはget操作を通じて一定時間のパフォーマンスを実現できますが、HashTableのputおよびget操作は synchronized ロックされているため、効率は非常に低くなります。

  • 初期容量は異なります。HashTableの初期長は11です。各拡張後、容量は前の2n + 1になります(nは前回の長さです)

    HashMapの初期の長さは16で、展開するたびに元の長さの2倍になります。作成時に、容量の初期値が指定されている場合、HashTableは指定されたサイズを直接使用し、HashMapはそれを2の累乗に拡張します。

HashMapとHashSetの違い

HashSetは、AbstractSetインターフェイスを継承し、Set、Cloneable、およびjava.io.Serializableインターフェイスを実装します。HashSetは、セット内の重複値を許可しません。HashSetの最下層は実際にはHashMapであり、HashSetでのすべての操作は実際にはHashMapでの操作です。したがって、HashSetはコレクションの順序を保証しません。

HashMapはどのように拡張されますか

HashMapには2つの非常に重要な変数があります。1つはloadFactor 、もう1つは 、  threshold loadFactorは負荷率を表し、thresholdは次の拡張しきい値を表します。threshold= loadFactor *配列の長さの場合、配列の長さは元の2つに拡張されます。Double、マップのサイズを変更し、元のオブジェクトを新しいバケット配列に配置します。

HashMapの長さが2の累乗である理由

HashMapの長さは2の累乗であるため、length%hash ==(n-1)&hashである理由。残りは、バケット内の添え字を決定するために使用されます。長さが2の累乗でない場合、友達は試してみる例をあげることができます

たとえば、長さが9、3&(9-1)= 0、2&(9-1)= 0の場合、両方が0になり、衝突します。

これにより、HashMapの衝突の可能性が高くなります。

HashMapスレッドセーフの実装は何ですか

HashMapはスレッドセーフなコンテナーではないため、並行シナリオConcurrentHashMap で使用するか、スレッドセーフなHashMapを使用 Collections するか、パッケージの下でスレッドセーフなコンテナーを使用することをお勧めします。 

Collections.synchronizedMap(new HashMap());

スレッドセーフなコンテナでもあるHashTableを使用することもできます。HashTableのデータ構造はHashMapと同じであるため、Key-Valueストレージに基づいてHashMapとHashTableが比較されることがよくあります。

おすすめ

転載: blog.csdn.net/feikillyou/article/details/112725411