はじめに
先日、新人研修のお手伝いをしていた際に、受講生が書いたコードの中で「HashMapのキーに配列(int[])を指定している」ものを見かけました。
実はこれ、私自身もかつて競技プログラミング(競プロ)に挑戦していた頃、多次元のグリッドや座標の状態を管理しようとして見事にはまった思い出のある、典型的な落とし穴です。競プロや研修の場ではこの手のコードが結構出てくるのですが、Javaではコンパイルエラーや実行時エラーにならずに「ただ意図した通りに値が取得できなくなる」ため、原因に気づきにくい難しさがあります。
原因:配列のhashCodeとequalsの挙動
HashMapは内部でキーを特定する際、オブジェクトのhashCode()メソッドとequals()メソッドを利用します。しかし、Javaの配列におけるこれらのメソッドは、要素の中身ではなくインスタンスの参照(メモリ上のアドレス)を基準に動作します。
-
equals(): 配列の内容が同じであっても、異なるインスタンスであれば
falseを返します。 - hashCode(): インスタンスごとに異なるハッシュ値を返します。
問題が発生するコード例
void main() {
Map<int[], String> map = new HashMap<>();
int[] key1 = new int[]{1, 2, 3};
map.put(key1, "value");
// key1と内容は同じだが、別インスタンスの配列を作成
int[] key2 = new int[]{1, 2, 3};
// 結果はnullとなる
IO.println(map.get(key2));
}
上記の例では、key1とkey2の中身はどちらも[1, 2, 3]ですが、インスタンスが異なるため、map.get(key2)で値を取り出すことができずnullが返ります。
解決策
複数の数値を組み合わせてキーにしたい場合は、要素の中身ベースで等価性を判定する型を使用します。
1. List を使用する
Listインターフェースの実装クラスは、要素の内容に基づいてequals()とhashCode()が計算されるように定義されています。List.ofメソッドを使用することで、要素の変更を禁止した不変(Immutable)なリストを簡潔に生成できます。
void main() {
Map<List<Integer>, String> map = new HashMap<>();
map.put(List.of(1, 2, 3), "value");
// 内容が同じであれば、別インスタンスでも値を取得できる
IO.println(map.get(List.of(1, 2, 3))); // 出力: value
}
2. KeyクラスとしてRecordを使用する
Javaではrecordを使用することで、各フィールドの値に基づいてequals()やhashCode()が自動的に生成されます。キーとしての意味付けが明確になり、パフォーマンスや堅牢性の面でも推奨されるアプローチです。
以下は、格納したい複数の要素を保持する不変のKeyクラスを定義して使用する例です。
void main() {
Map<Key, String> map = new HashMap<>();
map.put(new Key(1, 2, 3), "value");
// 各フィールドの値が同じであれば、別インスタンスでも値を取得できる
IO.println(map.get(new Key(1, 2, 3))); // 出力: value
}
// 複数の値をカプセル化するキー用のKeyクラスをレコードで定義
record Key(int x, int y, int z) {
}
環境
- Java 25