1. はじめに
Java には列挙型(enum)用の専用コレクションである EnumSet と EnumMap があり、標準ライブラリ(java.util)に含まれ、enum 型を極めて効率的に扱うために設計されています。
EnumSet と EnumMap の内部構造
EnumSet はビットマスクとして動作します。列挙要素がそれぞれちょうど 1 ビットを占めるフラグの集合を想像してください。ビットが 1 なら集合に含まれ、0 なら含まれません。すべては数値配列(long[])に保持され、enum の値が 64 未満であれば、集合全体がたった 1 個の数値に収まります。
EnumMap はさらに単純です。値の配列であり、インデックスには enum 要素の序数(ordinal)が使われます。従来の HashMap<Enum, V> の代わりに、とてもコンパクトで高速な構造が得られます。
何が得られるのでしょうか。操作(追加・削除・存在確認)はすべて O(1) で実行されます。メモリ使用量は最小限(特に HashSet や HashMap と比べて)です。要素の反復順は常に enum の宣言順で、結果が予測可能になります。
例: どのように見えるか
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
System.out.println(weekend); // [SAT, SUN]
外側からは普通の集合に見えます。しかし内部では 1 つの数値で 2 つのビットが立っています。1 つは SAT、もう 1 つは SUN 用です。FRI を追加すれば、さらに 1 ビットが立ちます。ハッシュテーブルや余分なオブジェクトは不要です。
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MON, "Gym");
schedule.put(Day.FRI, "Party");
System.out.println(schedule); // {MON=Gym, FRI=Party}
ここではキー(Day)が内部の配列インデックスに変換されるため、アクセスは配列要素への参照と同等に高速です。
2. ユースケース: フラグ、対応表、有限状態機械
EnumSet: フラグや状態集合に最適
- フラグ: 限られた数のオプションに対して「オン/オフ」の集合を保持。
- enum 値の集合: 曜日、ユーザー権限、タスク状態など。
- 有限状態機械 (FSM): 許可される遷移を EnumSet に保持すると便利。
例: アクセスフラグ
enum Permission { READ, WRITE, EXECUTE }
EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.WRITE)) {
// 書き込みが許可されている
}
例: いくつかを除いた全値
EnumSet<Day> workdays = EnumSet.complementOf(EnumSet.of(Day.SAT, Day.SUN));
System.out.println(workdays); // [MON, TUE, WED, THU, FRI]
EnumMap: enum キーのテーブルに最適
- 対応表: キーは enum の値、値は任意のオブジェクト。
- 高速アクセス: HashMap<Enum, V> より高速かつコンパクト。
例: 曜日別の価格
EnumMap<Day, Integer> prices = new EnumMap<>(Day.class);
prices.put(Day.MON, 100);
prices.put(Day.SAT, 200);
System.out.println(prices.get(Day.SAT)); // 200
例: 有限状態機械
enum State { START, RUNNING, STOPPED }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.START, EnumSet.of(State.RUNNING));
transitions.put(State.RUNNING, EnumSet.of(State.STOPPED));
transitions.put(State.STOPPED, EnumSet.noneOf(State.class));
3. 落とし穴: enum の変更とシリアライズ
EnumSet と EnumMap は、enum の要素の集合と順序に依存します。新しい要素の追加、既存要素の削除、順序の入れ替えを行うと、保存済みまたはシリアライズ済みのコレクションが正しく動作しない可能性があります。
シリアライズ
- EnumSet と EnumMap はシリアライズ可能ですが、シリアライズとデシリアライズの間に enum が変更されると、エラーやデータ損失が発生し得ます。
- シリアライズ後に enum の値を削除すると、読み込み時に例外が発生する可能性が非常に高いです。
ベストプラクティス:
- EnumSet/EnumMap は、enum が安定している確信がない場合はシリアライズしない。
- 長期保存には、たとえば値の文字列表現のリストなどを用いる。
4. 有用な注意点
EnumSet/EnumMap と通常のコレクションの比較
| コレクション | キー/要素 | 内部構造 | 性能 | メモリ | Null |
|---|---|---|---|---|---|
|
|
ビットマスク | O(1) | 非常に少ない | 不可 |
|
|
ハッシュテーブル | O(1) | 多い | 可 |
|
|
ordinal による配列 | O(1) | 非常に少ない | 不可 |
|
|
ハッシュテーブル | O(1) | 多い | 可 |
ベストプラクティス
- EnumSet を値の集合に使用する(of、noneOf、allOf、complementOf)。
- キーが enum の連想テーブルには EnumMap を使用する。
- 巨大な列挙型は避ける。数百要素の列挙はコンパクトさを損なう。
- enum が変わる可能性があるならシリアライズしない。
- null をキーや値として使用しない。
5. 実践: アプリケーションでの EnumSet と EnumMap の使い方
例: ユーザーのロールを保持する
enum Role { USER, ADMIN, MODERATOR }
class User {
private EnumSet<Role> roles = EnumSet.noneOf(Role.class);
public void addRole(Role role) {
roles.add(role);
}
public boolean isAdmin() {
return roles.contains(Role.ADMIN);
}
}
例: 状態遷移テーブル
enum State { NEW, IN_PROGRESS, DONE }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.NEW, EnumSet.of(State.IN_PROGRESS));
transitions.put(State.IN_PROGRESS, EnumSet.of(State.DONE));
transitions.put(State.DONE, EnumSet.noneOf(State.class));
6. EnumSet/EnumMap を扱う際の典型的な誤り
エラー 1: enum ではない型で EnumSet/EnumMap を使う。
これらのコレクションは、enum 型に対してのみ機能します。
// EnumSet<String> set = EnumSet.of("A", "B"); // コンパイルエラー!
エラー 2: 非常に大きな enum に対して EnumSet/EnumMap を使う。
数百の値を持つ列挙はコンパクトさを損ないます。それでも同じデータに対する HashSet/HashMap よりは、メモリ効率が良い場合が多いです。
エラー 3: シリアライズ後に enum を変更する。
シリアライズ後に値の追加・削除・並べ替えを行うと、読み込み時のエラーやデータ損失につながります。
エラー 4: null とともに EnumSet/EnumMap を使う。
EnumSet の要素も、EnumMap のキー/値も null にできません。
EnumSet<Day> days = EnumSet.of(null); // NullPointerException!
EnumMap<Day, String> map = new EnumMap<>(Day.class);
map.put(null, "test"); // NullPointerException!
エラー 5: EnumSet が「普通の」Set だと期待する。
EnumSet は自分の enum の値のみを保持します。列挙にない任意のオブジェクトを追加することはできません。
エラー 6: 「変更され得る」enum に EnumSet/EnumMap を使う。
列挙がランタイムに生成・差し替えられる(動的ロード/リフレクションなど)場合、これらの構造は正しく動作しません。
GO TO FULL VERSION