Dlaczego konstruktor enum nie może uzyskać dostępu do pól statycznych?


110

Dlaczego konstruktor enum nie może uzyskać dostępu do pól i metod statycznych? Jest to całkowicie poprawne w przypadku klasy, ale nie jest dozwolone w przypadku wyliczenia.

To, co próbuję zrobić, to przechowywać moje wystąpienia wyliczenia w statycznej mapie. Rozważmy poniższy przykładowy kod, który umożliwia wyszukiwanie według skrótu:

public enum Day {
    Sunday("Sun"), Monday("Mon"), Tuesday("Tue"), Wednesday("Wed"), Thursday("Thu"), Friday("Fri"), Saturday("Sat");

    private final String abbreviation;

    private static final Map<String, Day> ABBREV_MAP = new HashMap<String, Day>();

    private Day(String abbreviation) {
        this.abbreviation = abbreviation;
        ABBREV_MAP.put(abbreviation, this);  // Not valid
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    public static Day getByAbbreviation(String abbreviation) {
        return ABBREV_MAP.get(abbreviation);
    }
}

To nie zadziała, ponieważ wyliczenie nie zezwala na statyczne odwołania w swoim konstruktorze. Jednak działa po prostu znajdź, jeśli zaimplementowano jako klasę:

public static final Day SUNDAY = new Day("Sunday", "Sun");
private Day(String name, String abbreviation) {
    this.name = name;
    this.abbreviation = abbreviation;
    ABBREV_MAP.put(abbreviation, this);  // Valid
}

Odpowiedzi:


113

Konstruktor jest wywoływany, zanim wszystkie pola statyczne zostały zainicjowane, ponieważ pola statyczne (w tym te reprezentujące wartości wyliczeniowe) są inicjowane w kolejności tekstowej, a wartości wyliczenia zawsze występują przed innymi polami. Zwróć uwagę, że w przykładzie Twojej klasy nie pokazałeś, gdzie jest inicjalizowana ABBREV_MAP - jeśli jest po NIEDZIELA, otrzymasz wyjątek, gdy klasa zostanie zainicjowana.

Tak, to trochę uciążliwe i prawdopodobnie mogło zostać lepiej zaprojektowane.

Jednak typową odpowiedzią z mojego doświadczenia jest posiadanie static {}bloku na końcu wszystkich statycznych inicjatorów i wykonanie tam wszystkich statycznych inicjalizacji, używając EnumSet.allOf do uzyskania wszystkich wartości.


40
Jeśli dodasz klasę zagnieżdżoną, jej statystyka zostanie zainicjowana w odpowiednim czasie.
Tom Hawtin - tackline

Ooh, niezły. Nie pomyślałem o tym.
Jon Skeet

3
Trochę nieparzysty, ale jeśli wywołasz metodę statyczną w konstruktorze wyliczenia, który zwraca wartość statyczną, skompiluje się dobrze - ale wartość, którą zwróci, będzie wartością domyślną dla tego typu (tj. 0, 0,0, '\ u0000' lub null), nawet jeśli jawnie go ustawisz (chyba że jest zadeklarowany jako final). Zgadnij, że będzie to trudne do złapania!
Mark Rhodes,

2
szybkie pytanie dodatkowe @JonSkeet: Czy jest jakiś powód, którego używasz EnumSet.allOfzamiast Enum.values()? Pytam, ponieważ valuesjest to rodzaj metody fantomowej (nie widzę źródła Enum.class) i nie wiem, kiedy została utworzona
Chirlo

1
@Chirlo Jest pytanie w tej sprawie. Wydaje się, że Enum.values()jest to szybsze, jeśli planujesz iterację po nich z ulepszoną pętlą for (ponieważ zwraca tablicę), ale głównie chodzi o styl i przypadek użycia. Prawdopodobnie lepiej jest użyć, EnumSet.allOf()jeśli chcesz napisać kod, który istnieje w dokumentacji Javy, a nie tylko w specyfikacjach, ale i tak wiele osób wydaje się być zaznajomionych Enum.values().
4castle

31

Cytat z JLS, sekcja „Enum Body Declarations” :

Bez tej reguły pozornie rozsądny kod zakończyłby się niepowodzeniem w czasie wykonywania z powodu cykliczności inicjalizacji właściwej dla typów wyliczeniowych. (Cykliczność istnieje w każdej klasie z polem statycznym wpisanym samodzielnie). Oto przykład kodu, który mógłby się nie powieść:

enum Color {
    RED, GREEN, BLUE;
    static final Map<String,Color> colorMap = new HashMap<String,Color>();

    Color() {
       colorMap.put(toString(), this);
    }
}

Inicjalizacja statyczna tego typu wyliczenia spowodowałaby zgłoszenie wyjątku NullPointerException, ponieważ zmienna statyczna colorMap nie jest zainicjowana, gdy konstruktory dla stałych wyliczenia są uruchamiane. Powyższe ograniczenie gwarantuje, że taki kod nie zostanie skompilowany.

Zauważ, że przykład można łatwo refaktoryzować, aby działał poprawnie:

enum Color {
    RED, GREEN, BLUE;
    static final Map<String,Color> colorMap = new HashMap<String,Color>();

    static {
        for (Color c : Color.values())
            colorMap.put(c.toString(), c);
    }
}

Wersja refaktoryzowana jest wyraźnie poprawna, ponieważ statyczna inicjalizacja odbywa się od góry do dołu.


9

może tego chcesz

public enum Day {
    Sunday("Sun"), 
    Monday("Mon"), 
    Tuesday("Tue"), 
    Wednesday("Wed"), 
    Thursday("Thu"), 
    Friday("Fri"), 
    Saturday("Sat");

    private static final Map<String, Day> ELEMENTS;

    static {
        Map<String, Day> elements = new HashMap<String, Day>();
        for (Day value : values()) {
            elements.put(value.element(), value);
        }
        ELEMENTS = Collections.unmodifiableMap(elements);
    }

    private final String abbr;

    Day(String abbr) {
        this.abbr = abbr;
    }

    public String element() {
        return this.abbr;
    }

    public static Day elementOf(String abbr) {
        return ELEMENTS.get(abbr);
    }
}

Używanie Collections.unmodifiableMap()jest tutaj bardzo dobrą praktyką. +1
4castle

Dokładnie to, czego szukałem. Lubię też oglądać Collections.unmodifiableMap. Dziękuję Ci!
LethalLima

6

Problem rozwiązany za pomocą zagnieżdżonej klasy. Zalety: jest krótszy, a także lepszy pod względem zużycia procesora. Wady: jeszcze jedna klasa w pamięci JVM.

enum Day {

    private static final class Helper {
        static Map<String,Day> ABBR_TO_ENUM = new HashMap<>();
    }

    Day(String abbr) {
        this.abbr = abbr;
        Helper.ABBR_TO_ENUM.put(abbr, this);

    }

    public static Day getByAbbreviation(String abbr) {
        return Helper.ABBR_TO_ENUM.get(abbr);
    }

1

Gdy klasa jest ładowana do maszyny JVM, pola statyczne są inicjowane w kolejności, w jakiej pojawiają się w kodzie. Np

public class Test4 {
        private static final Test4 test4 = new Test4();
        private static int j = 6;
        Test4() {
            System.out.println(j);
        }
        private static void test() {
        }
        public static void main(String[] args) {
            Test4.test();
        }
    }

Na wyjściu będzie 0. Zwróć uwagę, że inicjalizacja test4 odbywa się w procesie inicjalizacji statycznej iw tym czasie j nie jest jeszcze inicjalizowana, jak to się później pojawi. Teraz, jeśli zmienimy kolejność statycznych inicjatorów tak, że j występuje przed test4. Wynik wyniesie 6, ale w przypadku Enum nie możemy zmienić kolejności pól statycznych. Pierwszą rzeczą w wyliczeniu muszą być stałe, które są w rzeczywistości statycznymi, końcowymi instancjami typu wyliczeniowego, dlatego w przypadku wyliczeń zawsze gwarantuje się, że pola statyczne nie zostaną zainicjowane przed stałymi wyliczenia, ponieważ nie możemy nadać żadnych sensownych wartości dla pól statycznych do użycia w konstruktorze wyliczenia , nie miałoby sensu uzyskiwanie do nich dostępu w konstruktorze wyliczenia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.