1. Введение
В Java есть специальные коллекции для работы с перечислениями (enum): EnumSet и EnumMap. Они входят в стандартную библиотеку (java.util) и предназначены для максимально эффективной работы с enum-типами.
Как устроены EnumSet и EnumMap внутри?
EnumSet работает как битовая маска. Представьте набор флажков, где каждый элемент перечисления занимает ровно один бит. Если бит включён — элемент есть в множестве, если выключен — его нет. Всё хранится в массиве чисел (long[]), и если в вашем enum меньше 64 значений, то весь набор помещается в одном-единственном числе!
EnumMap устроен ещё проще: это массив значений, где индексом служит порядковый номер (ordinal) элемента enum. Вместо привычного 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]
Снаружи это обычное множество. Но внутри — число, у которого выставлены два бита: один для SAT, другой для SUN. Добавите FRI — включится ещё один бит. Никакой хэш-таблицы и лишних объектов.
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 после сериализации почти гарантированно приведёт к исключению при чтении.
Best practice:
- Не сериализуйте EnumSet/EnumMap, если не уверены, что enum стабилен.
- Для долгого хранения используйте, например, список строковых представлений значений.
4. Полезные нюансы
Сравнение EnumSet/EnumMap и обычных коллекций
| Коллекция | Ключ/элемент | Внутреннее устройство | Производительность | Память | Null |
|---|---|---|---|---|---|
|
|
битовая маска | O(1) | очень мало | нельзя |
|
|
хэш-таблица | O(1) | больше | можно |
|
|
массив по ordinal | O(1) | очень мало | нельзя |
|
|
хэш-таблица | O(1) | больше | можно |
Best practices
- Используйте EnumSet для наборов значений (of, noneOf, allOf, complementOf).
- Используйте EnumMap для ассоциативных таблиц, где ключ — enum.
- Избегайте «гигантских» перечислений — перечни на сотни значений ухудшают компактность.
- Не сериализуйте, если 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: Использование EnumSet/EnumMap с не-enum-типом.
Эти коллекции работают только с типами, которые являются enum.
// EnumSet<String> set = EnumSet.of("A", "B"); // Ошибка компиляции!
Ошибка № 2: Использование EnumSet/EnumMap для очень больших enum.
Перечисления на сотни значений ухудшают компактность. Тем не менее это всё ещё чаще лучше по памяти, чем HashSet/HashMap для тех же данных.
Ошибка № 3: Изменение enum после сериализации.
Добавление/удаление/переупорядочивание значений после сериализации приводит к ошибкам при чтении или к потере данных.
Ошибка № 4: Использование EnumSet/EnumMap с null.
Ни элементы 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: Использование EnumSet/EnumMap для «изменяемых» enum.
Если перечисления генерируются или подменяются в рантайме (динамическая загрузка/рефлексия), эти структуры работать корректно не будут.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ