HashMap
はJavaで最もよく使われるデータ構造の一つで、その効率の良さで知られています。HashMap
のデータはキーと値のペアの形で格納されます。
この記事では、JavaのHashMap
を紹介します。HashMap
の一般的な操作について説明した後、内部的にどのように動作するのかを掘り下げていきます。ハッシュ関数とインデックス計算がどのように行われるかを理解します。
Javaにおける HashMap
とは
HashMap
は、Javaコレクションフレームワークの一部であるMap
インターフェイスを実装しています。
ハッシュは、ハッシュ関数を使用して、任意のサイズの入力を固定サイズの出力に変換する技術です。生成された出力はハッシュコードと呼ばれ、Javaでは整数値で表されます。
一般的な操作
上で説明したように、HashMap
内のデータはキーと値のペアの形で格納されます。
HashMap
でサポートされる一般的な操作を以下に示します。
挿入
- このメソッドは新しいキーと値のペアを
HashMap
に挿入します。 -
キーと値のペアの插入順序は保証されません。
-
插入中、既にキーが存在している場合、引数として渡された新しい値に既存の値が置き換えられます。
-
HashMap
には null キーを一度だけ插入できますが、null 値は複数指定できます。
この操作の方法のシグネチャは以下に示されます。その後、例があります。
public V put(K key, V value)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
上のコードでは、String型のキーとInteger型の値を追加するHashMap
の例があります。
取得:
-
指定されたキーに割り当てられた値を取得します。
-
HashMap
にキーが存在しない場合はnull
を返します。
この操作の方法のシグネチャは以下に示されます。その後、例があります。
public V get(Object key)
Integer value = map.get("apple"); // 1を返す
上のコードでは、apple
キーに割り当てられた値を取得しています。
他にも一般的な操作があります。
-
remove
:指定されたキーのキーと値のペアを削除します。キーが見つからない場合はnull
を返します。 -
containsKey
:指定されたキーがHashMap
に存在するかどうかを確認します。 -
containsValue
:指定された値がHashMap
に存在するかどうかを確認します。
HashMap
の内部構造
HashMap
は、バケツまたはビンの配列を内部的に使用します。各バケツは、Node
型のリンクリストであり、HashMap
のキーと値のペアを表します。
static class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
上記では、HashMap
のキーと値のペアを格納するために使用されるNode
クラスの構造が示されています。
Node
クラスには、以下のフィールドがあります:
-
hash
:キーのhashCode
を参照します。 -
key
:キーと値のペアのキーを参照します。 -
value
:キーに関連付けられた値を参照します。 -
next
: 次のノードへの参照を行います。
HashMapは根本的にhash tableの実装に基づいており、性能は初期容量と荷重係数に依存します。Hash tableクラスの原始的なjavadocsでは、これらの2つのパラメーターを以下のように定義しています。
-
容量はhash table内のバケットの数であり、初期容量はhash tableを作成したときの容量です。
-
荷重係数は、hash tableが自動的に拡大される前に满たすことができる程度の hash tableの満杯率を表します。
次に、HashMap
における基本的な操作、put
とget
の仕組みを理解しましょう。
ハッシュ関数
关键字と値のペアを插入(`put`)した際に、`HashMap`はまずキーのハッシュコードを計算します。このハッシュ関数は、キーに対する整数を計算します。クラスは`Object`クラスの`hashCode`メソッドを使用するか、このメソッドをオーバライドして独自の実装を提供することができます。ハッシュコードの約束についてはここを読むことができます。その後、ハッシュコードは位数の16ビットをXOR(排他的OR)运算して、より均等な分布を実現します。
XORは、2つのビットを比較し、異なる場合は1、同じ場合は0になるビット演算です。この場合、ハッシュコードとその上位16ビット(unsigned right shift `>>>>` 演算子を使用して得られる)の間でビット演算のXOR运算を行い、ビットを混ぜることで、より均等に分布するハッシュコードを得ます。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
上記は、`HashMap`クラスの静的なハッシュメソッドを示しています。
このアイデアは、各キーに固有のハッシュコードを持つことですが、ハッシュ関数は異なるキーに同じハッシュコードを生成することもあります。これは衝突と呼ばれる状況に導きます。衝突を処理する方法は次の節で説明します。
インデックス計算
キーのハッシュコードが生成された後、`HashMap`はバケットの配列内のインデックスを計算し、キーと値のペアが格納される場所を決定します。これは、配列の長さが2の幂である場合、模运算を効率的に行うためのビット演算として行われます。
int index = (n - 1) & hash;
ここで、
バケット配列の長さをnとして計算しています。インデックスを計算した後、キーはそのインデックスにバケット配列に保存されます。しかし、複数のキーが同じインデックスになると衝突が発生します。このような状況下で、HashMap
は2つの方法のいずれかで处理します。
-
Chaining/Linking: 配列内の各バケットはノードの LinkedList です。特定のインデックスに既にキーが存在し、もう一つのキーが同じインデックスにハッシュされた場合、それはリストに追加されます。
-
Treeify: ノードの数が一定の閾値を超えると、リンクリストが树状结构として変換されます(これは Java 8 で導入されました)。
static final int TREEIFY_THRESHOLD = 8;
これは treeification を決定する閾値です。
したがって、良いハッシュ関数を持っておくことは、キーをバケット間で平均的に分布させ、衝突の可能性を最小限にするために非常に重要です。
取得(get
)と削除(remove
)操作は、挿入(put
)操作と同様に行われます。以下はその方法です。
-
取得(
get
): ハッシュ関数を使用してハッシュコードを計算する -> ハッシュコードを使用してインデックスを計算する -> LinkedList または树状结构を穿越して、一致するキーのノードを找到する。 -
削除(
remove
):ハッシュ関数を使用してハッシュコードを計算し、その後、ハッシュコードを使用してインデックスを計算し、リンクリストまたは树からノードを削除する。
時間的な复杂度
HashMap
の基本的な操作、put
、get
、remove
などは、キーが均等に分布されることを前提に、常にO(1)の定数時間の性能を提供する。キーの分布が悪く、多くの衝突が発生する場合、これらの操作は線形時間の Complexity O(n)に衰える可能性があります。
トリー化によって、長い衝突の连锁をバランスのある树に変換することにより、Lookup操作の効率をより良い対数時間 Complexity O(log n)に改善することができます。
同期
HashMap
の実装は同期されていません。複数のスレッドが同一のHashMapインスタンスに并发してアクセスし、マップを巡回している場合、スレッドの中の1つがマップ上でキー-値のマッピングを追加または削除する structural modificationを行うと、ConcurrentModificationException
が発生します。
これを防止するためには、Collections.synchronizedMap
メソッドを使用してスレッド Safeなインスタンスを作成することができます。
結論
要するに、HashMap
の内部工作机制を理解することは、開発者が情報を持って決定をしたりすることが非常に重要です。键との対応、衝突の発生や避け方を知ることができるために、HashMap
を効果的に、効率的に使用することができます。
Source:
https://www.freecodecamp.org/news/how-java-hashmaps-work-internal-mechanics-explained/