Pourquoi le constructeur d'énumération ne peut-il pas accéder aux champs statiques?

110

Pourquoi le constructeur d'énumération ne peut-il pas accéder aux champs et méthodes statiques? Ceci est parfaitement valable avec une classe, mais n'est pas autorisé avec une énumération.

Ce que j'essaie de faire est de stocker mes instances enum dans une carte statique. Considérez cet exemple de code qui permet la recherche par abréviation:

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

Cela ne fonctionnera pas car enum n'autorise pas les références statiques dans son constructeur. Cela fonctionne cependant juste trouver s'il est implémenté en tant que 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
la source

Réponses:

113

Le constructeur est appelé avant que les champs statiques aient tous été initialisés, car les champs statiques (y compris ceux représentant les valeurs d'énumération) sont initialisés dans l'ordre textuel et les valeurs d'énumération viennent toujours avant les autres champs. Notez que dans votre exemple de classe, vous n'avez pas montré où ABBREV_MAP est initialisé - si c'est après SUNDAY, vous obtiendrez une exception lorsque la classe est initialisée.

Oui, c'est un peu pénible et aurait probablement pu être mieux conçu.

Cependant, la réponse habituelle dans mon expérience est d'avoir un static {}bloc à la fin de tous les initialiseurs statiques, et de faire toute l'initialisation statique là-bas, en utilisant EnumSet.allOf pour obtenir toutes les valeurs.

Jon Skeet
la source
40
Si vous ajoutez une classe imbriquée, la statique de celle-ci sera initialisée au moment opportun.
Tom Hawtin - tackline
Ooh, gentil. Je n'avais pas pensé à ça.
Jon Skeet le
3
Un peu bizarre mais si vous appelez une méthode statique dans un constructeur enum qui renvoie une valeur statique, elle se compilera correctement - mais la valeur qu'elle renvoie sera celle par défaut pour ce type (c'est-à-dire 0, 0.0, '\ u0000' ou null), même si vous le définissez explicitement (sauf s'il est déclaré comme final). Je suppose que ce sera difficile à attraper!
Mark Rhodes
2
Question dérivée rapide @JonSkeet: Une raison que vous utilisez à la EnumSet.allOfplace de Enum.values()? Je demande parce que valuesc'est une sorte de méthode fantôme ( Enum.classje ne peux pas voir la source ) et je ne sais pas quand elle a été créée
Chirlo
1
@Chirlo Il y a une question à ce sujet. Il semble que ce Enum.values()soit plus rapide si vous prévoyez de les parcourir avec une boucle for améliorée (car elle renvoie un tableau), mais il s'agit principalement du style et du cas d'utilisation. Il est probablement préférable de l'utiliser EnumSet.allOf()si vous souhaitez écrire du code qui existe dans la documentation de Java plutôt que dans les spécifications, mais de nombreuses personnes semblent le connaître de Enum.values()toute façon.
4castle
31

Citation de JLS, section "Enum Body Declarations" :

Sans cette règle, un code apparemment raisonnable échouerait au moment de l'exécution en raison de la circularité d'initialisation inhérente aux types enum. (Une circularité existe dans toute classe avec un champ statique "auto-typé".) Voici un exemple du type de code qui échouerait:

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

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

L'initialisation statique de ce type d'énumération lèverait une NullPointerException car la variable statique colorMap n'est pas initialisée lorsque les constructeurs des constantes d'énumération s'exécutent. La restriction ci-dessus garantit qu'un tel code ne se compilera pas.

Notez que l'exemple peut facilement être remanié pour fonctionner correctement:

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 version refactorisée est clairement correcte, car l'initialisation statique se produit de haut en bas.

Phani
la source
9

c'est peut-être ce que tu veux

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
la source
L'utilisation Collections.unmodifiableMap()est ici une très bonne pratique. +1
4castle
Exactement ce que je cherchais. J'aime aussi voir Collections.unmodifiableMap. Je vous remercie!
LethalLima
6

Le problème est résolu via une classe imbriquée. Avantages: il est plus court et meilleur par la consommation du processeur. Inconvénients: une classe de plus dans la mémoire 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
la source
1

Lorsqu'une classe est chargée dans la JVM, les champs statiques sont initialisés dans l'ordre dans lequel ils apparaissent dans le code. Pour par exemple

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 sortie sera 0. Notez que l'initialisation de test4 a lieu dans le processus d'initialisation statique et pendant ce temps j n'est pas encore initialisé comme il apparaîtra plus tard. Maintenant, si nous changeons l'ordre des initialiseurs statiques tels que j précède test4. La sortie sera 6.Mais dans le cas des Enums, nous ne pouvons pas modifier l'ordre des champs statiques. La première chose dans enum doit être les constantes qui sont en fait des instances finales statiques de type enum. , il serait inutile d'y accéder dans le constructeur enum.

Hitesh
la source