1. Wprowadzenie
W Javie istnieją specjalne kolekcje do pracy z wyliczeniami (enum): EnumSet i EnumMap. Wchodzą w skład standardowej biblioteki (java.util) i są przeznaczone do maksymalnie efektywnej pracy z typami enum.
Jak działają EnumSet i EnumMap pod spodem?
EnumSet działa jak maska bitowa. Wyobraź sobie zestaw przełączników, w którym każdy element wyliczenia zajmuje dokładnie jeden bit. Jeśli bit jest włączony — element jest w zbiorze, jeśli wyłączony — elementu nie ma. Wszystko jest przechowywane w tablicy liczb (long[]), a jeśli w Twoim enum jest mniej niż 64 wartości, cały zestaw mieści się w jednej jedynej liczbie!
EnumMap jest zbudowany jeszcze prościej: to tablica wartości, gdzie indeksem jest numer porządkowy (ordinal) elementu enum. Zamiast typowego HashMap<Enum, V> otrzymujesz bardzo kompaktową i szybką strukturę.
Co to daje? Operacje dodawania, usuwania i sprawdzania wykonywane są w O(1). Wykorzystywane jest minimum pamięci (zwłaszcza w porównaniu z HashSet i HashMap). Iteracja elementów zawsze przebiega w kolejności deklaracji w enum, co czyni wynik przewidywalnym.
Przykład: jak to wygląda
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
System.out.println(weekend); // [SAT, SUN]
Z zewnątrz to zwykły zbiór. Ale w środku — liczba, w której ustawione są dwa bity: jeden dla SAT, drugi dla SUN. Dodasz FRI — włączy się kolejny bit. Bez tabeli mieszającej i zbędnych obiektów.
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}
Tutaj klucze (Day) zamieniają się na wewnętrzne indeksy tablicy, dlatego dostęp działa tak szybko, jak odwołanie do elementu tablicy.
2. Przykłady użycia: flagi, tabele, automaty skończone
EnumSet: idealny do flag i zbiorów stanów
- Flagi: przechowywanie zestawów „włączone/wyłączone” dla ograniczonej liczby opcji.
- Zbiory wartości enum: dni tygodnia, uprawnienia użytkownika, stany zadania.
- Automaty skończone (FSM): wygodnie przechowywać dozwolone przejścia w EnumSet.
Przykład: flagi dostępu
enum Permission { READ, WRITE, EXECUTE }
EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.WRITE)) {
// Pisanie dozwolone
}
Przykład: wszystkie wartości poza niektórymi
EnumSet<Day> workdays = EnumSet.complementOf(EnumSet.of(Day.SAT, Day.SUN));
System.out.println(workdays); // [MON, TUE, WED, THU, FRI]
EnumMap: idealny do tablic po kluczach enum
- Tabele odwzorowań: kluczami są wartości enum, wartościami — dowolne obiekty.
- Szybki dostęp: szybszy i bardziej kompaktowy niż HashMap<Enum, V>.
Przykład: ceny według dni tygodnia
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
Przykład: automat skończony
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. Pułapki: zmiany w enum i serializacja
EnumSet i EnumMap zależą od składu i kolejności wartości Twojego enum. Jeśli dodasz nowy element, usuniesz stary lub zmienisz ich kolejność, zapisane lub zserializowane kolekcje mogą zachowywać się niepoprawnie.
Serializacja
- EnumSet i EnumMap są serializowalne, ale przy zmianie enum między serializacją a deserializacją mogą wystąpić błędy/utrata danych.
- Usunięcie wartości z enum po serializacji niemal na pewno doprowadzi do wyjątku podczas odczytu.
Najlepsza praktyka:
- Nie serializuj EnumSet/EnumMap, jeśli nie masz pewności, że enum jest stabilny.
- Do długotrwałego przechowywania użyj na przykład listy tekstowych reprezentacji wartości.
4. Przydatne szczegóły
Porównanie EnumSet/EnumMap i zwykłych kolekcji
| Kolekcja | Klucz/element | Wewnętrzna implementacja | Wydajność | Pamięć | Null |
|---|---|---|---|---|---|
|
|
maska bitowa | O(1) | bardzo mało | nie można |
|
|
tablica mieszająca | O(1) | więcej | można |
|
|
tablica indeksowana przez ordinal | O(1) | bardzo mało | nie można |
|
|
tablica mieszająca | O(1) | więcej | można |
Najlepsze praktyki
- Używaj EnumSet do zbiorów wartości (of, noneOf, allOf, complementOf).
- Używaj EnumMap do tabel asocjacyjnych, gdzie kluczem jest enum.
- Unikaj „gigantycznych” wyliczeń — setki wartości pogarszają kompaktowość.
- Nie serializuj, jeśli enum może się zmieniać.
- Nie używaj null jako klucza ani wartości.
5. Praktyka: jak używać EnumSet i EnumMap w aplikacji
Przykład: przechowywanie ról użytkownika
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);
}
}
Przykład: tabela przejść stanów
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. Typowe błędy przy pracy z EnumSet/EnumMap
Błąd nr 1: Użycie EnumSet/EnumMap z typem innym niż enum.
Te kolekcje działają wyłącznie z typami, które są enum.
// EnumSet<String> set = EnumSet.of("A", "B"); // Błąd kompilacji!
Błąd nr 2: Używanie EnumSet/EnumMap dla bardzo dużych enum.
Wyliczenia liczące setki wartości pogarszają kompaktowość. Mimo to wciąż często wypadają lepiej pamięciowo niż HashSet/HashMap dla tych samych danych.
Błąd nr 3: Zmiana enum po serializacji.
Dodanie/usunięcie/zmiana kolejności wartości po serializacji prowadzi do błędów przy odczycie lub do utraty danych.
Błąd nr 4: Używanie EnumSet/EnumMap z null.
Ani elementy EnumSet, ani klucze/wartości EnumMap nie mogą być null.
EnumSet<Day> days = EnumSet.of(null); // NullPointerException!
EnumMap<Day, String> map = new EnumMap<>(Day.class);
map.put(null, "test"); // NullPointerException!
Błąd nr 5: Oczekiwanie, że EnumSet to „zwykły” Set.
EnumSet przechowuje tylko wartości swojego enum; nie możesz dodać tam dowolnego obiektu spoza wyliczenia.
Błąd nr 6: Używanie EnumSet/EnumMap dla „zmiennych” enum.
Jeśli wyliczenia są generowane lub podmieniane w czasie działania (dynamiczne ładowanie/refleksja), te struktury nie będą działać poprawnie.
GO TO FULL VERSION