ハッシュマップの概要
HashMapは、ハッシュテーブルに基づいたMAPインターフェイスの非同期実装です。この実装は、すべてのオプションのマッピング操作を提供し、null値とnullキーを使用できます。このクラスは、マッピングの順序を保証するものではありません。特に、順序が続くことを保証するものではありません。
Hashmapのデータ構造
Javaプログラミング言語には、2つの基本構造があります。1つは配列で、もう1つはシミュレートされたポインターです(参照)。すべてのデータ構造は、これら2つの基本構造を使用して構築でき、ハッシュマップも例外ではありません。 Hashmapは、実際には「リンクリストハッシュ」データ構造、つまり配列とリンクリストの構造ですが、JDK1.8です。
赤と黒の木の実装が追加されます。リンクされたリストの長さが8を超えると、赤と黒の木の構造に変換されます。
上記の図からわかるように、HashmapはJavaのチェーンアドレス法を使用しています。リンクアドレスメソッドは、簡単に言えば、配列とリンクリストの組み合わせです。各配列要素にリンクされたリスト構造があります。データがハッシュされると、配列添え字が取得され、データが対応するサブスクリクト要素のリンクリストに配置されます。
*/ static class node <k、v>はmap.entry <k、v> {final int hash; //配列インデックスの位置を見つけるために使用される最終Kキー; v値; node <k、v> next; //リンクリストノードの次のノード(int hash、k key、v value、node <k、v> next){this.hash = hash; this.key = key; this.value = value; this.next = next; }ノードは、Hashmapの内部クラスであり、Map.entryインターフェイスを実装します。これは、基本的にマップ(キー値ペア)です。
時々、2つのキーが同じ位置に配置され、ハッシュ衝突が発生したことを示します。もちろん、ハッシュアルゴリズムの計算結果は均一であるほど、ハッシュ衝突の確率が小さく、マップのアクセス効率が高くなります。
ハッシュマップクラスには非常に重要なフィールドがあります。これはノード[]テーブル、つまりハッシュバケットアレイです。明らかにノードの配列です。
ハッシュバケットアレイが大きい場合、貧弱なハッシュアルゴリズムでさえ散乱します。ハッシュバケットアレイアレイアレイが小さい場合、優れたハッシュアルゴリズムでさえ衝突が増えるため、スペースコストと時間コストを比較検討する必要があります。実際、実際の状況に基づいてハッシュバケットアレイのサイズを決定することであり、これに基づいて、設計されたハッシュアルゴリズムはハッシュ衝突を減らします。では、ハッシュ衝突の確率を小さくするためにマップを制御するにはどうすればよいですか。答えは、優れたハッシュアルゴリズムと容量拡張メカニズムです。
ハッシュと拡張プロセスを理解する前に、ハッシュマップのいくつかのフィールドを理解する必要があります。 Hashmapのデフォルトコンストラクターのソースコードから、コンストラクターが次のフィールドを初期化することがわかります。ソースコードは次のとおりです。
intしきい値; //収容できるキー価値は、究極のフロートロードファクターです。 // Load Factor int modcount; intサイズ;
まず、ノード[]テーブルの初期化長(デフォルト値は16)、負荷係数は負荷係数(デフォルト値は0.75)、しきい値はハッシュマップが対応できる最大データ量のノード数(キー値ペア)の数です。しきい値=長さ *負荷係数。つまり、配列がその長さを定義した後、負荷係数が大きいほど、それが収容できるキー価値のペアが増えます。
負荷係数の定義式に基づいて、しきい値は、この負荷係数と長さ(配列の長さ)に応じて許可される要素の最大数であることがわかります。この数がこれを超える場合は、サイズを変更します(容量の拡大)。拡張されたハッシュマップ容量は、以前の容量の2倍です。デフォルトの負荷係数0.75は、スペースと時間効率のバランスの取れた選択肢です。特別な時間と空間の場合、多くのメモリスペースと高い時間効率要件がある場合、負荷係数荷重係数の値を減らすことができない限り、変更しないことをお勧めします。それどころか、メモリスペースがタイトで、時間効率の要件が高くない場合、負荷係数の値を増やすことができます。これは1を超えることがあります。
サイズフィールドは実際には理解しやすく、実際にハッシュマップに存在するキー価値のペアの数です。テーブルの長さと、最大のキー値ペアに対応するしきい値の違いに注意してください。 ModCountフィールドは、主にハッシュマップの内部構造の変化の数を記録するために使用され、主に反復の急速な障害に使用されます。強調するために、内部構造の変化は、新しいキー値のペアを配置するなど、構造の変化を指しますが、キーに対応する値は上書きされ、構造の変化に属していません。
ハッシュマップでは、ハッシュバケットアレイテーブルの長さは2のn電源でなければなりません(複合番号でなければなりません)。これは型破りなデザインです。従来のデザインは、バケツのサイズを素数として設計することです。比較的言えば、素数によって引き起こされる競合の確率は、複合数の可能性よりも小さくなっています。特定の証明については、//www.vevb.com/article/100911.htmを参照してください。ハッシュテーブルは、バケットサイズを11に初期化します。これは、素数として設計されたバケットサイズの適用です(ハッシュテーブルは、拡張後に素数であることを保証できません)。 Hashmapは、主にModuloと拡張時に最適化するために、この型破りな設計を採用しています。同時に、競合を減らすために、ハッシュマップは、ハッシュバケットインデックスの位置を見つけるときに、コンピューティングへのハイビット参加プロセスも追加します。
ここには問題があります。負荷係数とハッシュアルゴリズムが合理的に設計されていても、ジッパーが長すぎる状況は必然的にあります。ジッパーが長すぎると、ハッシュマップのパフォーマンスに深刻な影響を与えます。したがって、JDK1.8バージョンでは、データ構造がさらに最適化され、赤と黒の木が導入されました。リンクされたリストの長さが長すぎる場合(デフォルトは8を超える)、リンクされたリストは赤と黒の木に変換されます。赤と黒の木の迅速な追加、削除、修正、検索の特性を使用して、ハッシュマップのパフォーマンスを向上させます。赤と黒の木の挿入、削除、および検索は、赤や黒の木などのアルゴリズムを挿入、削除、検索するために使用されます。
ハッシュバケットアレイのインデックス位置を決定します
コード実装:
//方法1:Static Final Int Hash(Object Key){//jdk1.8&jdk1.7 int h; // h = key.hashcode()最初のステップのハッシュコード値を取得します0:(h = key.hashcode()) ^(h >>> 16);} //方法2:static int indexfor(int h、int length){//jdk1.7ソースコード、jdk1.8にはこのメソッドはありませんが、実装原則は同じ返品h&(長さ1)です。 // 3番目のステップでモジュラス操作を取得します}ここでのハッシュアルゴリズムは、本質的に3つのステップです。キーのハッシュコード値、ハイビット操作、および弾性操作です。
任意のオブジェクトの場合、HashCode()戻り値が同じである限り、プログラム呼び出し方式1つで計算されたハッシュコードは常に同じです。私たちが最初に考えることは、要素の分布が比較的均一になるように、アレイの長さのハッシュ値をモジュロすることです。ただし、弾性操作の消費は比較的大きいです。これは、HashMapで行われます。メソッド2を呼び出して、オブジェクトをテーブル配列に保存するインデックスを計算します。
この方法は非常に賢いです。 h&(table.length -1)を介してオブジェクトの保存されたビットを取得し、下にあるハッシュマップの配列の長さは常に2のn -powerになります。長さが常に2のn電力になる場合、H&(長さ1)動作は弾性の長さ、つまりh%長さに相当しますが、%よりも効率が高くなります。
JDK1.8の実装では、ハイビット操作のアルゴリズムが最適化されており、ハッシュコード():(h = k.hashcode()) ^(h >>> 16)のHigh-16ビットXOR Low-16-Bit実装は、主に速度、効率、品質から考慮されます。これにより、アレイテーブルの長さが比較的小さい場合、BITがハッシュ計算に関係していることを保証することができます。
これが例です。Nはテーブルの長さです。
HashMap Put Method実装
プット関数の一般的なアイデアは次のとおりです。
特定のコードは次のように実装されます。
public v put(k key、v value){return putval(hash(key)、key、value、false、true); } / ***ハッシュを生成する方法* / static final int hash(object key){int h; return(key == null)? 0:(h = key.hashcode()) ^(h >>> 16); } final v putval(int hash、k key、v value、booleanの唯一のファブセント、boolean evict){node <k、v> [] tab;ノード<k、v> p; int n、i; //テーブルが空であるかどうかを判断します。表[i] == nullの場合、新しいノードを直接作成してif((p = tab [i =(n -1)&hash])== null)tab [i] = newnode(hash、key、value、null); else {//対応するノードにノード<k、v> e; K K; //テーブル[i]の最初の要素がキーと同じかどうかを判断します。同じ場合、値を直接上書きする場合(p.hash == hash &&((k = p.key)== key ||(key!= null && key.equals(k)))e = p; //テーブル[i]がTreeNodeであるかどうか、つまり、テーブル[i]が赤黒の木かどうかを判断します。赤黒の木の場合は、キー値ペアをツリーに直接挿入します。 //このチェーンはリンクされたリストですelles {//テーブル[i]を通過して、リンクリストの長さがtreeify_thresholdよりも大きいかどうかを判断します(デフォルト値は8)。 8を超える場合は、リンクされたリストを赤黒樹に変換し、赤黒樹に挿入操作を実行します。それ以外の場合、リンクリストの挿入操作が実行されます。キーがすでに移動プロセス中に直接上書き値を持っていることがわかった場合。 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);壊す; } if(e.hash == hash &&((k = e.key)== key ||(key!= null && key.equals(k)))break; p = e; }} // write if(e!= null){//キーv oldvalue = e.valueの既存のマッピング; if(!onlyifabsent || oldvalue == null)e.value = value;午後のccess(e); OldValueを返します。 }} ++ modcount; //挿入が成功した後、キー値のペアの実際の数が最大容量のしきい値を超えるかどうかを判断します。容量を超える場合、if(++ size>しきい値)sezize()を展開します。 AfcordDodeInsertion(evict); nullを返します。 }HasHMAPメソッド実装を取得します
アイデアは次のとおりです。
1.バケット内の最初のノードは、直接ヒットします。
2.競合がある場合は、key.equals(k)を使用して対応するエントリを見つけます
ツリーの場合は、key.equals(k)、o(logn)を介してツリーを検索します。
リンクされたリストの場合は、リンクリストのkey.equals(k)を介して検索、o(n)。
public v get(object key){node <k、v> e; return(e = getNode(hash(key)、key))== null? null:e.value; } final node <k、v> getNode(int hash、object key){node <k、v> [] tab; node <k、v> first、e; int n; K K; if((tab = table)!= null &&(n = tab.length)> 0 &&(first = tab [(n -1)&hash])!= null){//直接hit if(first.hash == hash && // first nodeがチェックされるとき(k = first.key)== key(k) // if((e = first.next)!= null){// get if(first instanceof treenode)return((treeNode <k、v>)first).getTreenode(hash、key); // get do {if(e.hash == hash &&((k = e.key)== key ||(key!= null && key.equals(k)))return e; } while((e = e.next)!= null); }} nullを返します。 }容量拡張メカニズム
サイズは、容量を再計算し、ハッシュマップオブジェクトに常に要素を追加することを意味します。 Hashmapオブジェクト内の配列がより多くの要素をロードできない場合、オブジェクトは、より多くの要素をロードできるように配列の長さを拡張する必要があります。もちろん、Javaの配列を自動的に拡張することはできません。この方法は、小さなバケツを使用して水を満たすように、既存のアレイではなく、既存のアレイの代わりに新しい配列を使用することです。より多くの水を満たしたい場合は、大きなバケツに置き換える必要があります。
サイズのソースコードを分析します。 JDK1.8が赤と黒の木を統合することを考えると、それはより複雑です。理解を促進するために、まだ理解しやすいJDK1.7コードを使用しています。本質にほとんど違いはありません。後で特定の違いについて話しましょう。
void resize(int newcapacity){//新しい容量エントリ[] oldtable = table; //拡張前にエントリアレイを参照int oldcapacity = oldtable.length; if(oldcapacity == maximing_capacity){//拡張前の配列サイズが最大(2^30)しきい値= integer.max_valueに達した場合。 //しきい値をint(2^31-1)の最大値に変更して、将来のリターンで容量が拡大しないようにします。 } entry [] NewTable = new Entry [NewCapacity]; //新しいエントリアレイ転送(NewTable)を初期化します。 //! !データを新しいエントリアレイテーブル= newTableに転送します。 // hashmapのテーブル属性は、新しいエントリアレイしきい値=(int)(newcapacity * loadfactor)を指します。 //しきい値を変更}ここでは、容量が小さい既存の配列ではなく、より大きな容量のアレイを使用します。 Transf()メソッドは、元のエントリアレイの要素を新しいエントリアレイにコピーします。
void transfer(entry [] newtable){entry [] src = table; // srcは、古いエントリアレイint newcapacity = newtable.lengthを指します。 for(int j = 0; j <src.length; j ++){//古いエントリアレイエントリを介した静けさ<k、v> e = src [j]; //古いエントリアレイの各要素を取得する場合は(e!= null){src [j] = null; //古いエントリアレイのオブジェクト参照をリリースします(forループの後、古いエントリアレイは任意のオブジェクトを参照しなくなります){entry <k、v> next = e.next; int i = indexfor(e.hash、newcapacity); //! !配列の各要素の位置を再計算しますe.next = newtable [i]; // tag [1] newtable [i] = e; // array e = nextに要素を配置します。 //次のエントリチェーンの要素にアクセス} while(e!= null); }}}NewTable [i]への参照はe.Nextに割り当てられます。つまり、単一のリンクリストのヘッダー挿入方法が使用されます。同じ位置の新しい要素は、常にリンクリストのヘッドに配置されます。このようにして、インデックスに配置された要素は、最終的にエントリチェーンの最後に配置されます(ハッシュ競合が発生した場合)。これはJDK1.8とは異なり、以下で詳しく説明しています。古い配列の同じエントリチェーンの要素は、インデックス位置を再計算した後、新しい配列の異なる位置に配置できます。
容量拡張プロセスを説明する例を以下に示します。ハッシュアルゴリズムがキーmodを使用してテーブルのサイズ(つまり、配列の長さ)を取得するだけであると仮定します。ハッシュバケットアレイテーブルのサイズ= 2、したがって、キー= 3、7、5、およびプット順序は5、7、および3です。モッド2後、競合は表にあります[1]。ここでは、荷重係数loadFactor = 1、つまりキー値ペアの実際のサイズがテーブルの実際のサイズよりも大きい場合、拡張されていると想定されています。次の3つのステップは、ハッシュバケットアレイを4に変更し、すべてのノードを再ハッシュするプロセスです。
JDK1.8でどのような最適化が行われたかを説明しましょう。観察後、2つの拡張のパワー(元の2倍の長さを参照)を使用していることがわかります。したがって、要素の位置は元の位置にあるか、元の位置で2つのパワーの位置を再び移動します。下の図を見ると、この文の意味を理解できます。 nはテーブルの長さです。図(a)は、拡張前にkey1とkey2のインデックス位置を決定する2つのキーのインデックス位置の例を表しています。図(b)は、拡張後のkey1およびkey2のインデックス位置の例を表しています。ここで、HASH1はHASHおよびHIGHBIT動作の結果であり、Key1に対応します。
要素がハッシュを再計算した後、Nは2倍になるため、N-1のマスク範囲はハイポイントで1ビット(赤)であるため、新しいインデックスは次のように変更されます。
したがって、ハッシュマップを拡張する場合、JDK1.7の実装のようなハッシュを再計算する必要はありません。元のハッシュ値に追加されたビットが1または0であるかどうかを確認する必要があります。0の場合、インデックスは変更されていません。 1の場合、インデックスは「元のインデックス + oldcap」になります。次の図は、32への16の拡張を持つサイズ変更図として見ることができます。
このデザインは確かに非常に巧妙であり、ハッシュ値を再計算する時間を節約するだけでなく、新しく追加された1ビットは0または1であるため、ランダムと見なすことができるため、サイズのプロセスは以前の競合するノードを新しいバケツに均等に分散します。これは、JDK1.8によって追加された新しい最適化ポイントです。違いに少し注意が払われています。 JDK1.7で再ハッシュすると、古いリンクリストが新しいリンクリストを移行する場合、新しいテーブルの配列インデックス位置が同じ場合、リンクされたリスト要素が反転しますが、上記の図からわかるように、JDK1.8は反転しません。興味のある学生は、JDK1.8のサイズ変更ソースコードを研究できます。これは次のように非常に優れています。
final node <k、v> [] resize(){node <k、v> [] oldtab = table; int oldcap =(oldtab == null)? 0:oldtab.length; int oldthr =しきい値; int newcap、newthr = 0; if(oldcap> 0){//最大値が超えている場合、拡張されなくなるため、(oldcap> = maximinal_capacity){threathold = integer.max_value; Oldtabを返します。 } //最大値を超えていない場合、元のifの2倍に拡張されます((newcap = oldcap << 1)<maximix_capacity && oldcap> = default_initial_capacity)newthr = oldthr << 1; // doubleしきい値else {//ゼロ初期しきい値は、defaults newcap = default_initial_capacityを使用して意味します。 newthr =(int)(default_load_factor * default_initial_capacity); } //新しいサイズの上限を計算するif(newthr == 0){float ft =(float)newcap * loadfactor; newthr =(newCap <Maximion_Capacity && ft <(float)maximion_capacity?(int)ft:integer.max_value); }しきい値= newthr; @suppresswarnings({"rawTypes"、 "un -checked"})node <k、v> [] newtab =(node <k、v> [])new node [newcap];テーブル= newtab; if(oldtab!= null){//各バケットを新しいバケツに移動します(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; {next = e.next; //元のインデックスif((e.hash&oldcap)== 0){if(lotail == null)lohead = e; else lotail.next = e; lotail = e; } //オリジナルインデックス + oldcap else {if(hitail == null)hihead = e; else hitail.next = e; hitail = e; }} while((e = next)!= null); //元のインデックスをバケツに入れます(lotail!= null){lotail.next = null; newtab [j] = lohead; } //元のインデックス + oldcapをバケットに入れますif(hitail!= null){hitail.next = null; newtab [j + oldcap] = hihead; }}}}} newtabを返します;}要約します
ハッシュマップの理解を深めるために、最初にいくつかの質問に答えることができます。
1. Hashmapはいつ使用されますか?彼の特徴は何ですか?
マップインターフェイスの実装に基づいています。キー価値のペアを保存すると、非同期のnullキー値を受信できます。 HashMap Store Entry(Hash、Key、Value、Next)オブジェクト。
2。ハッシュマップがどのように機能するか知っていますか?
ハッシュメソッドを介して、オブジェクトはPut and Getを通じて保存および取得されます。オブジェクトを保存するとき、K/VをPUTメソッドに渡すと、ハッシュコードを呼び出してハッシュを計算してバケットの場所を取得し、さらに保存します。 HashMapは、現在のバケット職業に応じて容量を自動的に調整します(負荷FACOTRを超える場合、サイズは元の2倍です)。オブジェクトを取得すると、Kを渡して渡します。これは、ハッシュコードを呼び出してハッシュを計算してバケット位置を取得し、さらにequals()メソッドを呼び出してキー値ペアを決定します。衝突が発生した場合、ハッシュマップは、リンクされたリストを介して衝突競合を生成する要素を整理します。 Java 8では、バケットに衝突する要素が一定の制限を超えている場合(デフォルトは8)、赤と黒の木を使用してリンクリストを置き換えて速度を上げます。
3。Get and Putの原則を知っていますか? equals()とhashcode()の関数は何ですか?
キーのハッシュコード()をハッシュし、添え字(n-1&hash)を計算することにより、バケットの位置が得られます。衝突が発生した場合は、key.equals()メソッドを使用して、リンクされたリストまたはツリーで対応するノードを検索します
4。ハッシュの実装を知っていますか?なぜこれをする必要があるのですか?
Java 1.8の実装では、HashCode():(h = k.hashcode()) ^(h >>> 16)の16ビットx-16ビットを介して実装されます。これは、主に速度、効率、品質から考慮されます。これにより、バケットのNが比較的小さい場合、ハッシュ計算に高ビットと低ビットの両方が関与し、オーバーヘッドが過剰になることも保証できます。
5.ハッシュマップのサイズが負荷係数によって定義される容量を超える場合はどうなりますか?
負荷係数を超えている場合(デフォルト0.75)、元の長さの2倍のハッシュマップがサイズ変更され、ハッシュメソッドが再び呼び出されます。
Javaコレクションに関するチートシートは次のように説明されています。
エントリ[]アレイで実装されたハッシュバケットアレイとキーのハッシュ値を使用して、配列の添え字を取得できます。
要素を挿入すると、2つのキーが同じバケツに分類される場合(たとえば、ハッシュ値1と17がモジュロ16の後、両方とも最初のハッシュバケットに属します)、エントリは次のプロパティを使用して、一方向リンクリストに複数のエントリを実装し、バケツの現在のエントリの隣のバケットポイントに入るエントリを実装します。
ハッシュ値が17のキーを探している場合は、最初に最初のハッシュバケットを見つけてから、リンクリストでバケット内のすべての要素を反復し、キー値を1つずつ比較します。
エントリ数がバケットの75%に達すると(多くの記事が使用されるバケットの数が75%に達すると言っていますが、コードによると、バケットアレイは指数関数的に拡張され、元のエントリはすべて再割り当てされます。
ビット操作(Hash&(ArrayLength-1))はより速くなるため、配列のサイズは常に2のn電源になります。17などの初期値を与えると、32に変換されます。
Iterator()は、ハッシュバケットアレイに沿って横断します。
6. 2つのオブジェクトのハッシュコードが同じである場合はどうなりますか?
ハッシュコードは同じであるため、バケットの位置は同じであり、「衝突」が発生します。 HashMapはリンクリストを使用してオブジェクトを保存するため、このエントリ(キー価値ペアを含むMap.Entryオブジェクト)はリンクリストに保存されます。
7. 2つのキーのハッシュコードが同じ場合、どのように値オブジェクトを取得しますか?
バケットの位置を見つけた後、keys.equals()メソッドが呼び出され、リンクされたリストで正しいノードを見つけ、最後に値オブジェクトが見つかります。したがって、主要なタイプのハッシュマップを設計すると、不変のオブジェクトが最終的に宣言され、適切な等しい()メソッドが使用され、衝突の発生が減少し、効率が改善されます。不変性は、異なるキーのハッシュコードをキャッシュする可能性があり、オブジェクト全体を取得する速度が向上します。 StringやIntergerなどのラッパークラスをキーとして使用することは非常に良い選択です。
8.ハッシュマップのサイズが負荷係数によって定義される容量を超える場合はどうなりますか?
デフォルトの負荷係数サイズは0.75です。つまり、マップが他のコレクションクラス(アレイリストなどなど)と同様に75%のバケットを埋めると、元のハッシュマップの2倍のサイズのバケットアレイが作成され、元のオブジェクトを新しいバケットアレイに入れるために作成されます。このプロセスは、ハッシュメソッドを呼び出して新しいバケツの場所を見つけるため、再ハッシングと呼ばれます
9。ハッシュマップのサイズを変更することの何が問題なのか理解していますか?
ハッシュマップを変更するとき、実際には条件付き競争があります。両方のスレッドがハッシュマップのサイズを変更する必要があることがわかった場合、同時にサイズ変更を試みます。サイズ変更プロセス中、リンクリストに保存されている要素の順序は逆になります。これは、新しいバケット位置に移動すると、ハッシュマップはリンクリストの最後に要素を配置するのではなく、テールの移動を避けるためです。条件付き競争が発生した場合、悪循環があります。したがって、同時環境では、CurrentHashmapを使用してハッシュマップを置き換えます
10。弦やインターガーなどのラッパークラスがキーとして適しているのはなぜですか?
文字列は不変で最終的であり、equals()およびhashcode()メソッドが書き換えられているためです。他のラッパークラスにもこの機能があります。 HashCode()を計算するには、キー値が変更されないようにする必要があるため、不変性が必要です。キー値が、入力して取得するときに別のハッシュコードを返す場合、ハッシュマップから必要なオブジェクトを見つけることができません。不変性には、スレッドの安全性などの他の利点があります。フィールドをファイナルとして宣言するだけでハッシュコードが変わらないことを保証できる場合は、そうしてください。 equals()およびhashcode()メソッドはオブジェクトを取得するときに使用されるため、これら2つのメソッドを正しく書き換えることが非常に重要です。 2つの不均等なオブジェクトが異なるハッシュコードを返すと、衝突の可能性が小さくなり、ハッシュマップの性能が向上します
読んでくれてありがとう、私はそれがあなたを助けることができることを願っています。このサイトへのご支援ありがとうございます!