Por que o construtor de enum não pode acessar campos estáticos?

110

Por que o construtor de enum não pode acessar campos e métodos estáticos? Isso é perfeitamente válido com uma classe, mas não é permitido com um enum.

O que estou tentando fazer é armazenar minhas instâncias de enum em um mapa estático. Considere este código de exemplo que permite a pesquisa por abbreivação:

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

Isso não funcionará, pois enum não permite referências estáticas em seu construtor. No entanto, funciona apenas para descobrir se implementado como uma classe:

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
}
Steve Kuo
fonte

Respostas:

113

O construtor é chamado antes de todos os campos estáticos terem sido inicializados, porque os campos estáticos (incluindo aqueles que representam os valores enum) são inicializados em ordem textual e os valores enum sempre vêm antes dos outros campos. Observe que em seu exemplo de classe você não mostrou onde ABBREV_MAP é inicializado - se for depois de SUNDAY, você obterá uma exceção quando a classe for inicializada.

Sim, é um pouco chato e provavelmente poderia ter sido projetado melhor.

No entanto, a resposta usual em minha experiência é ter um static {}bloco no final de todos os inicializadores estáticos e fazer toda a inicialização estática lá, usando EnumSet.allOf para obter todos os valores.

Jon Skeet
fonte
40
Se você adicionar uma classe aninhada, a estática dessa classe será inicializada no momento apropriado.
Tom Hawtin - tackline
Ooh, boa. Eu não tinha pensado nisso.
Jon Skeet
3
Um pouco estranho, mas se você chamar um método estático em um construtor de enum que retorna um valor estático, ele compilará bem - mas o valor que retornar será o padrão para esse tipo (ou seja, 0, 0,0, '\ u0000' ou null), mesmo se você defini-lo explicitamente (a menos que seja declarado como final). Acho que vai ser difícil de pegar!
Mark Rhodes
2
pergunta rápida complementar @JonSkeet: Algum motivo para usar em EnumSet.allOfvez de Enum.values()? Eu pergunto porque valuesé uma espécie de método fantasma (não consigo ver a fonte Enum.class) e não sei quando foi criado
Chirlo
1
@Chirlo Há uma pergunta sobre isso. Parece que Enum.values()é mais rápido se você planeja iterar sobre eles com um loop for aprimorado (uma vez que retorna um array), mas principalmente se trata de estilo e caso de uso. Provavelmente é melhor usar EnumSet.allOf()se você deseja escrever código que existe na documentação do Java em vez de apenas nas especificações, mas muitas pessoas parecem estar familiarizadas de Enum.values()qualquer maneira.
4castle de
31

Citação de JLS, seção "Declarações de corpo de enum" :

Sem essa regra, o código aparentemente razoável falharia em tempo de execução devido à circularidade de inicialização inerente aos tipos de enum. (Existe uma circularidade em qualquer classe com um campo estático "autotipado".) Aqui está um exemplo do tipo de código que falharia:

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

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

A inicialização estática desse tipo de enum lançaria um NullPointerException porque a variável estática colorMap não foi inicializada quando os construtores para as constantes de enum são executados. A restrição acima garante que tal código não seja compilado.

Observe que o exemplo pode ser facilmente refatorado para funcionar corretamente:

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

A versão refatorada está claramente correta, pois a inicialização estática ocorre de cima para baixo.

Phani
fonte
9

talvez seja isso que você quer

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
fonte
Usar Collections.unmodifiableMap()é uma prática muito boa aqui. +1
4castle
Exatamente o que eu estava procurando. Também gosto de ver Collections.unmodifiableMap. Obrigado!
LethalLima
6

O problema foi resolvido por meio de uma classe aninhada. Prós: é mais curto e também melhor pelo consumo de CPU. Contras: mais uma classe de memória 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);
    }
Pavel Vlasov
fonte
1

Quando uma classe é carregada na JVM, os campos estáticos são inicializados na ordem em que aparecem no código. Por exemplo

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

A saída será 0. Observe que a inicialização de test4 ocorre no processo de inicialização estática e, durante esse tempo, j ainda não foi inicializado como aparecerá mais tarde. Agora, se mudarmos a ordem dos inicializadores estáticos de forma que j venha antes de test4. A saída será 6. Mas no caso de Enums, não podemos alterar a ordem dos campos estáticos. A primeira coisa em enum devem ser as constantes, que são, na verdade, instâncias finais estáticas do tipo enum. Assim, para enums, é sempre garantido que os campos estáticos não sejam inicializados antes das constantes de enum. Visto que não podemos fornecer nenhum valor sensível a campos estáticos para uso no construtor de enum , não faria sentido acessá-los no construtor enum.

Hitesh
fonte