BiMap は「キーと値の両方から引ける Map」
普通の Map<K, V> は「キー → 値」の片方向だけです。
でも業務では、「値からも逆引きしたい」場面がよく出てきます。
HTTPステータスコード 200 → “OK”
“OK” → 200 も知りたい
社員番号 12345 → “田中”
“田中” → 12345 も知りたい(重複しない前提なら)
こういうときに欲しくなるのが BiMap(バイマップ)、
つまり「キー → 値」と「値 → キー」を両方サポートする Map です。
Java標準には BiMap はありませんが、Map<K, V> と Map<V, K> を組み合わせれば、
実務で十分使える BiMap を自作できます。
基本設計:「2つの Map を常に同期させる」
片方は key→value、もう片方は value→key
BiMap のコアアイデアはとてもシンプルです。
片方の Map で「キー → 値」を管理する。
もう片方の Map で「値 → キー」を管理する。
put / remove のたびに、両方を同時に更新する。
この「2つの Map を常に矛盾なく保つ」ことが、BiMap 実装の一番大事なポイントです。
もう一つ重要なのは、
「値も一意(ユニーク)である」 という前提です。
同じ値に対して複数のキーがぶら下がると、「値 → キー」が一意に決まりません。
BiMap は「キーも値も一意」という世界で使う道具だ、と覚えておいてください。
最小限の BiMap 実装
コード全体をまず見てみる
import java.util.HashMap;
import java.util.Map;
public class BiMap<K, V> {
private final Map<K, V> forward = new HashMap<>();
private final Map<V, K> backward = new HashMap<>();
public void put(K key, V value) {
// 既存のキーがあれば、古い値との対応を削除
if (forward.containsKey(key)) {
V oldValue = forward.get(key);
backward.remove(oldValue);
}
// 既存の値があれば、古いキーとの対応を削除
if (backward.containsKey(value)) {
K oldKey = backward.get(value);
forward.remove(oldKey);
}
// 新しい対応を両方に登録
forward.put(key, value);
backward.put(value, key);
}
public V getByKey(K key) {
return forward.get(key);
}
public K getByValue(V value) {
return backward.get(value);
}
public void removeByKey(K key) {
V value = forward.remove(key);
if (value != null) {
backward.remove(value);
}
}
public void removeByValue(V value) {
K key = backward.remove(value);
if (key != null) {
forward.remove(key);
}
}
public boolean containsKey(K key) {
return forward.containsKey(key);
}
public boolean containsValue(V value) {
return backward.containsKey(value);
}
public int size() {
return forward.size();
}
public boolean isEmpty() {
return forward.isEmpty();
}
}
Javaここから、重要な部分を一つずつかみ砕いていきます。
重要ポイント1:put で「両方の Map の整合性」を必ず保つ
既存のキー・値があった場合の扱いが肝
put の中身を丁寧に見ていきます。
public void put(K key, V value) {
if (forward.containsKey(key)) {
V oldValue = forward.get(key);
backward.remove(oldValue);
}
if (backward.containsKey(value)) {
K oldKey = backward.get(value);
forward.remove(oldKey);
}
forward.put(key, value);
backward.put(value, key);
}
Javaここでやっていることは、次のような流れです。
キーがすでに使われていたら、そのキーに紐づいていた「古い値」を backward から消す。
値がすでに使われていたら、その値に紐づいていた「古いキー」を forward から消す。
最後に、新しい key↔value の対応を forward / backward 両方に登録する。
これにより、常に次の条件が保たれます。
forward に (k → v) があるとき、backward には必ず (v → k) がある。
backward に (v → k) があるとき、forward には必ず (k → v) がある。
キーも値も、それぞれ一意である。
この「put の中で整合性を全部面倒見る」設計がとても大事です。
呼び出し側は「BiMap に put すれば、キーと値の両方の世界がきれいに保たれる」と信じて使えます。
重要ポイント2:remove も「片方を消したら、もう片方も必ず消す」
片側だけ残ると BiMap ではなくなる
removeByKey を見てみます。
public void removeByKey(K key) {
V value = forward.remove(key);
if (value != null) {
backward.remove(value);
}
}
Javaforward からキーを消したあと、
対応する値があれば backward からも消しています。
removeByValue も同じ構造です。
public void removeByValue(V value) {
K key = backward.remove(value);
if (key != null) {
forward.remove(key);
}
}
Javaここでのポイントは、
「どちら側から消しても、必ずもう片方も追いかけて消す」
というルールを BiMap の中に閉じ込めていることです。
これを忘れると、
「キーからは引けないのに、値からは引ける」
「値からは引けないのに、キーからは引ける」
といった中途半端な状態になり、BiMap の意味が崩れます。
具体例1:HTTPステータスコード ↔ メッセージの BiMap
「コードからもメッセージからも引きたい」典型パターン
HTTPステータスコードとメッセージの対応を BiMap で管理してみます。
public class HttpStatusBiMapExample {
public static void main(String[] args) {
BiMap<Integer, String> status = new BiMap<>();
status.put(200, "OK");
status.put(404, "Not Found");
status.put(500, "Internal Server Error");
System.out.println(status.getByKey(200)); // OK
System.out.println(status.getByKey(404)); // Not Found
System.out.println(status.getByValue("OK")); // 200
System.out.println(status.getByValue("Not Found")); // 404
}
}
Javaここでのポイントは、
「コード → メッセージ」「メッセージ → コード」の両方が自然に書けていることです。
もし BiMap がなければ、Map<Integer, String> と Map<String, Integer> を別々に管理し、
両方に同じデータを入れる必要があります。
BiMap に「整合性の管理」を任せることで、
呼び出し側のコードがかなりスッキリします。
具体例2:ID ↔ 名前の BiMap(Enum 的な使い方)
「ID も名前もユニーク」なマスタデータに向いている
例えば、「権限ID ↔ 権限名」の対応を BiMap で持つ例です。
public class RoleBiMapExample {
public static void main(String[] args) {
BiMap<Integer, String> roles = new BiMap<>();
roles.put(1, "ADMIN");
roles.put(2, "USER");
roles.put(3, "GUEST");
System.out.println(roles.getByKey(1)); // ADMIN
System.out.println(roles.getByValue("USER")); // 2
// 値を更新するときも、BiMap が両側を調整してくれる
roles.put(2, "MEMBER"); // "USER" は消え、2 ↔ MEMBER に置き換わる
System.out.println(roles.getByValue("USER")); // null
System.out.println(roles.getByValue("MEMBER")); // 2
}
}
Javaここでの重要ポイントは二つです。
一つ目は、「ID も名前もユニークである」という前提が BiMap と相性が良いこと。
同じ名前を複数のIDに割り当てたいなら BiMap は不適切です。
二つ目は、「値の更新時に古い対応を自動で消してくれる」こと。roles.put(2, "MEMBER") のとき、
古い "USER" ↔ 2 の対応を BiMap がきちんと片付けてくれます。
スレッドセーフにしたい場合の考え方
単純に synchronized をかけるパターン
この BiMap はスレッドセーフではありません。
複数スレッドから同時に put / remove / get されるなら、同期が必要です。
一番簡単なのは、メソッドを synchronized にすることです。
public class BiMap<K, V> {
private final Map<K, V> forward = new HashMap<>();
private final Map<V, K> backward = new HashMap<>();
public synchronized void put(K key, V value) {
// さきほどと同じロジック
}
public synchronized V getByKey(K key) {
return forward.get(key);
}
public synchronized K getByValue(V value) {
return backward.get(value);
}
public synchronized void removeByKey(K key) {
...
}
public synchronized void removeByValue(V value) {
...
}
public synchronized int size() {
return forward.size();
}
public synchronized boolean isEmpty() {
return forward.isEmpty();
}
}
Javaここでのポイントは、
「forward と backward の両方を一貫性のある状態で扱うために、BiMap 全体をロックする」
という発想です。
片方の Map だけをロックしても意味がありません。
BiMap は「2つで1セット」なので、
「BiMap という箱全体」を同期の単位にするのが素直です。
まとめ:BiMap実装で身につけてほしい感覚
BiMap は、
単に「Map を2つ持ったクラス」ではなく、
「キーと値の両方向の対応を、常に矛盾なく保つためのユーティリティ」 です。
内部に Map<K, V> と Map<V, K> を持ち、put / remove で両方を必ず更新する。
キーも値も一意である前提のときに使う(そうでないなら MultiMap など別の道具を選ぶ)。
「コード ↔ 名前」「ID ↔ ラベル」「ステータスコード ↔ メッセージ」など、双方向参照したいマスタデータに当てはめる。
スレッドセーフにしたいなら、BiMap 全体を同期の単位として設計する。
あなたのコードのどこかに、Map<A, B> と Map<B, A> を手で同期させている箇所があれば、
そこを一度「BiMap クラス」にまとめられないか眺めてみてください。
それが、「よく出る双方向パターンに名前を与えて、再利用できる形にする」
エンジニアへの、いい一歩になります。
