Почему конструктор enum не может получить доступ к статическим полям?

110

Почему конструктор enum не может получить доступ к статическим полям и методам? Это совершенно верно для класса, но не допускается для перечисления.

Я пытаюсь сохранить экземпляры перечисления на статической карте. Рассмотрим этот пример кода, который позволяет выполнять поиск по сокращению:

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);
    }
}

Это не сработает, поскольку enum не допускает статических ссылок в своем конструкторе. Однако он работает просто найти, если реализован как класс:

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
}
Стив Куо
источник

Ответы:

113

Конструктор вызывается до того, как все статические поля были инициализированы, потому что статические поля (включая те, которые представляют значения перечисления) инициализируются в текстовом порядке, а значения перечисления всегда идут перед другими полями. Обратите внимание, что в вашем примере класса вы не показали, где инициализируется ABBREV_MAP - если это будет после ВОСКРЕСЕНЬЯ, вы получите исключение при инициализации класса.

Да, это немного неудобно, и, вероятно, его можно было бы разработать лучше.

Однако, по моему опыту, обычный ответ - иметь static {}блок в конце всех статических инициализаторов и выполнять там всю статическую инициализацию, используя EnumSet.allOf для получения всех значений.

Джон Скит
источник
40
Если вы добавите вложенный класс, его статика будет инициализирована в подходящее время.
Том Хотин - tackline
Ох, милый. Я не думал об этом.
Джон Скит,
3
Немного странно, но если вы вызываете статический метод в конструкторе перечисления, который возвращает статическое значение, он будет компилироваться нормально, но возвращаемое значение будет значением по умолчанию для этого типа (т.е. 0, 0.0, '\ u0000' или null), даже если вы явно задали его (если он не объявлен как final). Думаю, поймать его будет сложно!
Марк Роудс
2
быстрый дополнительный вопрос @JonSkeet: по какой причине вы используете EnumSet.allOfвместо Enum.values()? Я спрашиваю, потому что valuesэто своего рода фантомный метод (не вижу источник Enum.class), и я не знаю, когда он был создан
Chirlo
1
@Chirlo Есть вопрос по этому поводу. Кажется, что это Enum.values()будет быстрее, если вы планируете повторять их с помощью расширенного цикла for (поскольку он возвращает массив), но в основном это касается стиля и варианта использования. Вероятно, лучше использовать, EnumSet.allOf()если вы хотите написать код, который существует в документации Java, а не только в спецификациях, но многие люди, похоже, в Enum.values()любом случае знакомы с ним .
4castle
31

Цитата из JLS, раздел «Объявления тела перечисления» :

Без этого правила очевидный разумный код не смог бы работать во время выполнения из-за цикличности инициализации, присущей перечисляемым типам. (Цикличность существует в любом классе с «самотипированным» статическим полем.) Вот пример кода, который завершится ошибкой:

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

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

Статическая инициализация этого типа перечисления вызовет исключение NullPointerException, поскольку статическая переменная colorMap не инициализируется при запуске конструкторов для констант перечисления. Приведенное выше ограничение гарантирует, что такой код не будет компилироваться.

Обратите внимание, что пример можно легко отредактировать для правильной работы:

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);
    }
}

Реорганизованная версия явно верна, так как статическая инициализация происходит сверху вниз.

Фани
источник
9

может это то, что ты хочешь

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);
    }
}
user4767902
источник
Использование Collections.unmodifiableMap()здесь является очень хорошей практикой. +1
4castle
Именно то, что я искал. Мне также нравится видеть Collections.unmodifiableMap. Спасибо!
LethalLima
6

Проблема решена с помощью вложенного класса. Плюсы: короче и лучше по потреблению ЦП. Минусы: еще один класс в памяти 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

Когда класс загружается в JVM, статические поля инициализируются в том порядке, в котором они появляются в коде. Например,

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();
        }
    }

На выходе будет 0. Обратите внимание, что инициализация test4 происходит в процессе статической инициализации, и в это время j еще не инициализирован, как он появится позже. Теперь, если мы изменим порядок статических инициализаторов так, чтобы j был перед test4. На выходе будет 6. Но в случае Enums мы не можем изменить порядок статических полей. Первым делом в enum должны быть константы, которые на самом деле являются статическими конечными экземплярами типа enum. Таким образом, для перечислений всегда гарантируется, что статические поля не будут инициализированы перед константами перечисления. Поскольку мы не можем давать какие-либо разумные значения для статических полей для использования в конструкторе перечисления , было бы бессмысленно обращаться к ним в конструкторе перечисления.

Hitesh
источник