web-dev-qa-db-fra.com

Comment fournir la valeur Enum à une annotation à partir d'une constante dans Java

Je ne peux pas utiliser un Enum extrait d'une constante comme paramètre dans une annotation. J'obtiens cette erreur de compilation: "La valeur de l'attribut d'annotation [attribut] doit être une expression constante enum".

Il s'agit d'une version simplifiée du code pour l'énumération:

public enum MyEnum {
    Apple, ORANGE
}

Pour l'annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface MyAnnotation {
    String theString();

    int theInt();

    MyEnum theEnum();
}

Et la classe:

public class Sample {
    public static final String STRING_CONSTANT = "hello";
    public static final int INT_CONSTANT = 1;
    public static final MyEnum MYENUM_CONSTANT = MyEnum.Apple;

    @MyAnnotation(theEnum = MyEnum.Apple, theInt = 1, theString = "hello")
    public void methodA() {

    }

    @MyAnnotation(theEnum = MYENUM_CONSTANT, theInt = INT_CONSTANT, theString = STRING_CONSTANT)
    public void methodB() {

    }

}

L'erreur n'apparaît que dans "theEnum = MYENUM_CONSTANT" sur la méthodeB. Les constantes string et int sont correctes avec le compilateur, la constante Enum ne l'est pas, même si c'est exactement la même valeur que celle de la méthode A. Il me semble que c'est une fonctionnalité manquante dans le compilateur, car les trois sont évidemment des constantes. Il n'y a pas d'appels de méthode, pas d'utilisation étrange de classes, etc.

Ce que je veux réaliser, c'est:

  • Pour utiliser MYENUM_CONSTANT dans l'annotation et plus tard dans le code.
  • Pour rester en sécurité.

Tout moyen d'atteindre ces objectifs serait parfait.

Modifier:

Merci a tous. Comme vous le dites, cela ne peut pas être fait. Le JLS doit être mis à jour. J'ai décidé d'oublier les énumérations dans les annotations cette fois et d'utiliser des constantes int régulières. Tant que l'int est attribué à partir d'une constante nommée, les valeurs sont bornées et son type est "sûr".

Cela ressemble à ceci:

public interface MyEnumSimulation {
    public static final int Apple = 0;
    public static final int ORANGE = 1;
}
...
public static final int MYENUMSIMUL_CONSTANT = MyEnumSimulation.Apple;
...
@MyAnnotation(theEnumSimulation = MYENUMSIMUL_CONSTANT, theInt = INT_CONSTANT, theString = STRING_CONSTANT)
public void methodB() {
...

Et je peux utiliser MYENUMSIMUL_CONSTANT n'importe où ailleurs dans le code.

62
user1118312

Il semble être défini dans le JLS # 9.7.1 :

[...] Le type de V est compatible d'affectation (§5.2) avec T, et de plus:

  • [...]
  • Si T est un type enum et V est une constante enum.

Et une constante d'énumération est définie comme la constante d'énumération réelle ( JLS # 8.9.1 ), pas une variable qui pointe vers cette constante.

Conclusion: si vous souhaitez utiliser une énumération comme paramètre pour votre annotation, vous devrez lui donner une valeur explicite MyEnum.XXXX. Si vous souhaitez utiliser une variable, vous devrez choisir un autre type (pas une énumération).

Une solution de contournement possible consiste à utiliser un String ou int que vous pouvez ensuite mapper à votre énumération - vous perdrez la sécurité du type mais les erreurs peuvent être facilement repérées lors de l'exécution (= pendant les tests).

20
assylias

"Tous les problèmes en informatique peuvent être résolus par un autre niveau d'indirection" --- David Wheeler

C'est ici:

Classe d'énumération:

public enum Gender {
    MALE(Constants.MALE_VALUE), FEMALE(Constants.FEMALE_VALUE);

    Gender(String genderString) {
    }

    public static class Constants {
        public static final String MALE_VALUE = "MALE";
        public static final String FEMALE_VALUE = "FEMALE";
    }
}

Classe de personne:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = Person.GENDER)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Woman.class, name = Gender.Constants.FEMALE_VALUE),
    @JsonSubTypes.Type(value = Man.class, name = Gender.Constants.MALE_VALUE)
})
public abstract class Person {
...
}
86
Ivan Hristov

Je pense que la réponse la plus votée est incomplète, car cela ne garantit pas du tout que la valeur enum est couplée avec la constante sous-jacente String value . Avec cette solution, il suffit de découpler les deux classes.

Au lieu de cela, je suggère plutôt de renforcer le couplage montré dans cette réponse en imposant la corrélation entre le nom de l'énumération et la valeur constante comme suit:

public enum Gender {
    MALE(Constants.MALE_VALUE), FEMALE(Constants.FEMALE_VALUE);

    Gender(String genderString) {
      if(!genderString.equals(this.name()))
        throw new IllegalArgumentException();
    }

    public static class Constants {
        public static final String MALE_VALUE = "MALE";
        public static final String FEMALE_VALUE = "FEMALE";
    }
}

Comme indiqué par @ GhostCat dans un commentaire, des tests unitaires appropriés doivent être mis en place pour assurer le couplage.

19
JeanValjean

La règle de contrôle semble être "Si T est un type enum et V est une constante enum.", 9.7.1. Annotations normales . D'après le texte, il semble que le JLS vise une évaluation extrêmement simple des expressions dans les annotations. Une constante enum est spécifiquement l'identifiant utilisé dans la déclaration enum.

Même dans d'autres contextes, une finale initialisée avec une constante enum ne semble pas être une expression constante. 4.12.4. Variables finales dit "Une variable de type primitif ou de type String, qui est finale et initialisée avec une expression constante au moment de la compilation (§15.28), est appelée une variable constante.", Mais n'inclut pas une finale de type enum initialisée avec une constante enum.

J'ai également testé un cas simple dans lequel il importe si une expression est une expression constante - un si entourant une affectation à une variable non affectée. La variable n'est pas devenue affectée. Une version alternative du même code qui a plutôt testé un entier final a rendu la variable définitivement attribuée:

  public class Bad {

    public static final MyEnum x = MyEnum.AAA;
    public static final int z = 3;
    public static void main(String[] args) {
      int y;
      if(x == MyEnum.AAA) {
        y = 3;
      }
  //    if(z == 3) {
  //      y = 3;
  //    }
      System.out.println(y);
    }

    enum MyEnum {
      AAA, BBB, CCC
    }
  }
7

Je cite la dernière ligne de la question

Tout moyen d'atteindre ces objectifs serait parfait.

Alors j'ai essayé ça

  1. Ajout d'un paramètre enumType à l'annotation en tant qu'espace réservé

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    public @interface MyAnnotation {
    
        String theString();
        int theInt();
        MyAnnotationEnum theEnum() default MyAnnotationEnum.Apple;
        int theEnumType() default 1;
    }
    
  2. Ajout d'une méthode getType dans l'implémentation

    public enum MyAnnotationEnum {
        Apple(1), ORANGE(2);
        public final int type;
    
        private MyAnnotationEnum(int type) {
            this.type = type;
        }
    
        public final int getType() {
            return type;
        }
    
        public static MyAnnotationEnum getType(int type) {
            if (type == Apple.getType()) {
                return Apple;
            } else if (type == ORANGE.getType()) {
                return ORANGE;
            }
            return Apple;
        }
    }
    
  3. Apporté une modification pour utiliser une constante int au lieu de l'énumération

    public class MySample {
        public static final String STRING_CONSTANT = "hello";
        public static final int INT_CONSTANT = 1;
        public static final int MYENUM_TYPE = 1;//MyAnnotationEnum.Apple.type;
        public static final MyAnnotationEnum MYENUM_CONSTANT = MyAnnotationEnum.getType(MYENUM_TYPE);
    
        @MyAnnotation(theEnum = MyAnnotationEnum.Apple, theInt = 1, theString = "hello")
        public void methodA() {
        }
    
        @MyAnnotation(theEnumType = MYENUM_TYPE, theInt = INT_CONSTANT, theString = STRING_CONSTANT)
        public void methodB() {
        }
    
    }
    

Je dérive la constante MYENUM de MYENUM_TYPE int, donc si vous changez MYENUM, il vous suffit de changer la valeur int en la valeur de type enum correspondante.

Ce n'est pas la solution la plus élégante, mais je la donne à cause de la dernière ligne de la question.

Tout moyen d'atteindre ces objectifs serait parfait.

Juste une remarque, si vous essayez d'utiliser

public static final int MYENUM_TYPE = MyAnnotationEnum.Apple.type;

Le compilateur dit à l'annotation- MyAnnotation.theEnumType doit être une constante

2
Aditya

Ma solution était

public enum MyEnum {

    FOO,
    BAR;

    // element value must be a constant expression
    // so we needs this hack in order to use enums as
    // annotation values
    public static final String _FOO = FOO.name();
    public static final String _BAR = BAR.name();
}

Je pensais que c'était la façon la plus propre. Cela répond à quelques exigences:

  • Si vous voulez que les énumérations soient numériques
  • Si vous voulez que les énumérations soient d'un autre type
  • Le compilateur vous avertit si un refactor référence une valeur différente
  • Cas d'utilisation le plus propre (moins un caractère): @Annotation(foo = MyEnum._FOO)

MODIFIER

Cela conduit parfois à une erreur de compilation, ce qui conduit à la raison de l'original element value must be a constant expression

Ce n'est donc apparemment pas une option!

0
DKo