Java 6のソースを見ると、HashSet<E>
は実際にはHashMap<E,Object>
を使用して実装されており、セットのすべてのエントリでダミーオブジェクトインスタンスを使用しています。
エントリ自体のサイズに4バイト(32ビットマシンの場合)を浪費すると思います。
しかし、なぜそれがまだ使用されているのですか?コードの保守を容易にする以外に、それを使用する理由はありますか?
実際、それはHashSet
だけではありません。 Java 6のSet
インターフェースのすべての実装は、基礎となるMap
。これは必須ではありません。これは、実装の方法です。 Set
のさまざまな実装のドキュメントを確認すると、自分で確認できます。
あなたの主な質問は
しかし、なぜそれがまだ使用されているのですか?コードの保守を容易にする以外に、それを使用する理由はありますか?
コードのメンテナンスが大きな動機付けになると思います。重複や膨張を防ぎます。
Set
とMap
は類似したインターフェースであり、重複する要素は許可されていません。 (Set
に裏打ちされたMap
notはCopyOnWriteArraySet
だけだと思います。不変であるため、珍しいコレクションです。)
具体的には:
Set
のドキュメント から:
重複する要素を含まないコレクション。より正式には、セットには、e1.equals(e2)のような要素e1とe2のペアは含まれず、最大で1つのnull要素が含まれます。その名前が示すように、このインターフェイスは数学的なセットの抽象化をモデル化します。
Setインターフェースは、Collectionインターフェースから継承されたもの以外に、すべてのコンストラクターのコントラクト、およびadd、equals、hashCodeメソッドのコントラクトに追加の規定を配置します。便宜上、他の継承されたメソッドの宣言もここに含まれています。 (これらの宣言に付随する仕様は、Setインターフェースに合わせて調整されていますが、追加の規定は含まれていません。)
コンストラクターに関する追加の規定は、当然のことながら、すべてのコンストラクターは、重複する要素を含まないセットを作成する必要があることです(上記で定義)。
そして Map
から:
キーを値にマップするオブジェクト。マップに重複するキーを含めることはできません。各キーは最大で1つの値にマップできます。
既存のコードを使用してSet
sを実装できる場合、既存のコードから実現できるメリット(速度など)はSet
にも発生します。
Set
バッキングなしでMap
を実装することを選択した場合は、要素の重複を防ぐように設計されたコードを複製する必要があります。ああ、おいしい皮肉。
とはいえ、Set
sを別の方法で実装することを妨げるものは何もありません。
私の推測では、HashSetは元々、HashMapの観点から実装されており、迅速かつ簡単に実行できます。コード行に関しては、HashSetはHashMapの一部です。
まだ最適化されていないのは、変化への恐れからだと思います。
しかし、無駄はあなたが思っているよりはるかに悪いです。 32ビットと64ビットの両方で、HashSetは必要なサイズの4倍、HashMapは必要なサイズの2倍です。 HashMapは、キーと値を含む配列(および衝突用のチェーン)を使用して実装できます。これは、エントリごとに2つのポインタ、または64ビットVMでは16バイトを意味します。実際、HashMapには、エントリごとにEntryオブジェクトが含まれています。これにより、Entryへのポインタに8バイト、Entryオブジェクトヘッダーに8バイトが追加されます。 HashSetも要素ごとに32バイトを使用しますが、要素ごとに8バイトしか必要としないため、無駄は2倍ではなく4倍になります。
実際のアプリケーションや重要なベンチマークにとって重大な問題として発生したことは一度もないと思います。なぜ実際の利益のためにコードを複雑にするのですか?
また、多くのJVM実装ではオブジェクトのサイズが切り上げられているため、実際にはサイズが大きくならない場合があることにも注意してください(この例ではわかりません)。また、HashMap
のコードはコンパイルされてキャッシュに入れられる可能性があります。他の条件が同じであれば、コードが増える=>キャッシュミスが増える=>パフォーマンスが低下します。
はい、その通りです。そこには少量の無駄があります。すべてのエントリに対して同じオブジェクトPRESENT
(finalとして宣言されている)を使用するため、小さいです。したがって、無駄になるのは、HashMap内のすべてのエントリの値だけです。
ほとんどの場合、彼らは保守性と再利用性のためにこのアプローチを採用したと思います。 (JCF開発者は、とにかくHashMapをテストしたので、それを再利用しないのではないかと考えたでしょう。)
しかし、あなたが膨大なコレクションを持っていて、あなたがメモリフリークであるなら、あなたは Trove または GoogleCollections のようなより良い選択肢をオプトアウトするかもしれません。
私はあなたの質問を見て、あなたが言ったことを考えるのに少し時間がかかりました。 HashSet
の実装に関する私の意見です。
値がセットに存在するかどうかを知るために、ダミーインスタンスが必要です。
Addメソッドを見てください
_public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
_
Abdでは、putの戻り値を見てみましょう。
@は、キーに関連付けられた以前の値を返します。キーのマッピングがなかった場合はnullを返します。 (nullリターンは、マップが以前にnullをキーに関連付けたことを示す場合もあります。)
したがって、PRESENT
オブジェクトは、セットにe値が含まれていることを表すために使用されます。 null
の代わりにPRESENT
を使用しない理由を尋ねられたと思います。ただし、map.put(key,value)
は常にnull
を返し、キーが存在するかどうかを知る方法がないため、エントリが以前にマップ上にあったかどうかを区別することはできません。
そうは言っても、彼らはこのような実装を使用できたと主張することができます
_ public boolean add(E e) {
if( map.containsKey(e) ) {
return false;
}
map.put(e, null);
return true;
}
_
キーのhashCodeを2回計算するのを避けるために、4バイトを浪費していると思います(キーが追加される場合)。
たった4の同様のエントリを使用する他のデータ構造の代わりに(_Map.Entry
_のために)8バイトを浪費するHashMap
を使用した理由について質問する場合は、そうです。彼らはあなたが言及した理由でそれをしました。
このようなページを検索した後、なぜやや非効率的な標準実装なのか疑問に思い、com.carrotsearch.hppc.IntOpenHashSetを見つけました