¿Por qué el constructor de enum no puede acceder a campos estáticos?

110

¿Por qué el constructor de enum no puede acceder a campos y métodos estáticos? Esto es perfectamente válido con una clase, pero no está permitido con una enumeración.

Lo que estoy tratando de hacer es almacenar mis instancias de enumeración en un mapa estático. Considere este código de ejemplo que permite la búsqueda por abreviatura:

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

Esto no funcionará ya que enum no permite referencias estáticas en su constructor. Sin embargo, funciona solo busque si se implementa como una clase:

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
fuente

Respuestas:

113

Se llama al constructor antes de que se hayan inicializado todos los campos estáticos, porque los campos estáticos (incluidos los que representan los valores de enumeración) se inicializan en orden textual y los valores de enumeración siempre vienen antes que los otros campos. Tenga en cuenta que en su ejemplo de clase no ha mostrado dónde se inicializa ABBREV_MAP; si es después del DOMINGO, obtendrá una excepción cuando se inicialice la clase.

Sí, es un poco molesto y probablemente podría haber sido diseñado mejor.

Sin embargo, la respuesta habitual en mi experiencia es tener un static {}bloque al final de todos los inicializadores estáticos y realizar toda la inicialización estática allí, utilizando EnumSet.allOf para obtener todos los valores.

Jon Skeet
fuente
40
Si agrega una clase anidada, las estáticas de esa se inicializarán en el momento apropiado.
Tom Hawtin - tackline
Oh, bonita. No había pensado en eso.
Jon Skeet
3
Un poco extraño, pero si llama a un método estático en un constructor de enumeración que devuelve un valor estático, se compilará bien, pero el valor que devuelva será el predeterminado para ese tipo (es decir, 0, 0.0, '\ u0000' o null), incluso si lo configura explícitamente (a menos que se declare como final). ¡Supongo que será difícil de atrapar!
Mark Rhodes
2
pregunta derivada rápida @JonSkeet: ¿Alguna razón por la que usa en EnumSet.allOflugar de Enum.values()? Pregunto porque valueses una especie de método fantasma (no puedo ver la fuente Enum.class) y no sé cuándo se creó
Chirlo
1
@Chirlo Hay una duda al respecto. Parece que Enum.values()es más rápido si planea iterar sobre ellos con un bucle for mejorado (ya que devuelve una matriz), pero principalmente se trata de estilo y caso de uso. Probablemente sea mejor usarlo EnumSet.allOf()si desea escribir código que existe en la documentación de Java en lugar de solo en las especificaciones, pero muchas personas parecen estar familiarizadas con él de Enum.values()todos modos.
4castle
31

Cita de JLS, sección "Declaraciones del cuerpo de enumeración" :

Sin esta regla, el código aparentemente razonable fallaría en tiempo de ejecución debido a la circularidad de inicialización inherente a los tipos de enumeración. (Existe una circularidad en cualquier clase con un campo estático "autotipado"). A continuación, se muestra un ejemplo del tipo de código que fallaría:

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

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

La inicialización estática de este tipo de enumeración arrojaría una NullPointerException porque la variable estática colorMap no se inicializa cuando se ejecutan los constructores de las constantes de enumeración. La restricción anterior asegura que dicho código no se compilará.

Tenga en cuenta que el ejemplo se puede refactorizar fácilmente para que funcione correctamente:

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

La versión refactorizada es claramente correcta, ya que la inicialización estática se produce de arriba a abajo.

Phani
fuente
9

tal vez esto es lo que quieres

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);
    }
}
usuario4767902
fuente
El uso Collections.unmodifiableMap()es una muy buena práctica aquí. +1
4castle
Exactamente lo que estaba buscando. También me gusta ver Collections.unmodifiableMap. ¡Gracias!
LethalLima
6

El problema se resolvió mediante una clase anidada. Ventajas: es más corto y también mejor por el consumo de CPU. Contras: una clase más en la memoria 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
fuente
1

Cuando se carga una clase en la JVM, los campos estáticos se inicializan en el orden en que aparecen en el código. Por ejemplo

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

La salida será 0. Tenga en cuenta que la inicialización de test4 tiene lugar en el proceso de inicialización estático y durante este tiempo j aún no se ha inicializado como aparece más adelante. Ahora, si cambiamos el orden de los inicializadores estáticos de modo que j venga antes de test4. La salida será 6. Pero en el caso de Enums no podemos alterar el orden de los campos estáticos. Lo primero en enum deben ser las constantes que en realidad son instancias finales estáticas del tipo enum.Por lo tanto, para las enumeraciones siempre se garantiza que los campos estáticos no se inicializarán antes que las constantes enum.Ya que no podemos dar ningún valor sensible a los campos estáticos para su uso en el constructor de enum. , no tendría sentido acceder a ellos en el constructor enum.

Hitesh
fuente