So geben Sie einer Annotation aus einer Konstante in Java einen Enum-Wert an

76

Ich kann eine Aufzählung aus einer Konstante nicht als Parameter in einer Anmerkung verwenden. Ich erhalte den folgenden Kompilierungsfehler: "Der Wert für das Annotationsattribut [Attribut] muss ein konstanter Enum-Ausdruck sein".

Dies ist eine vereinfachte Version des Codes für die Aufzählung:

public enum MyEnum {
    APPLE, ORANGE
}

Für die Anmerkung:

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

    int theInt();

    MyEnum theEnum();
}

Und die Klasse:

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() {

    }

}

Der Fehler wird nur in "theEnum = MYENUM_CONSTANT" über Methode B angezeigt. String- und Int-Konstanten sind mit dem Compiler in Ordnung, die Enum-Konstante jedoch nicht, obwohl sie genau den gleichen Wert wie die über methodA hat. Sieht für mich so aus, als ob dies eine fehlende Funktion im Compiler ist, da alle drei offensichtlich Konstanten sind. Es gibt keine Methodenaufrufe, keine seltsame Verwendung von Klassen usw.

Was ich erreichen möchte, ist:

  • Verwendung von MYENUM_CONSTANT sowohl in der Anmerkung als auch später im Code.
  • Um typsicher zu bleiben.

Jeder Weg, um diese Ziele zu erreichen, wäre in Ordnung.

Bearbeiten:

Vielen Dank an alle. Wie Sie sagen, kann es nicht getan werden. Das JLS sollte aktualisiert werden. Diesmal habe ich beschlossen, Aufzählungen in Anmerkungen zu vergessen und reguläre int-Konstanten zu verwenden. Solange das int von einer benannten Konstante zugewiesen wird, sind die Werte begrenzt und es ist eine Art Typ sicher.

Es sieht aus wie das:

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() {
...

Und ich kann MYENUMSIMUL_CONSTANT an einer anderen Stelle im Code verwenden.

user1118312
quelle
Siehe auch
AlikElzin-kilaka

Antworten:

24

Es scheint in JLS # 9.7.1 definiert zu sein :

[...] Der Typ von V ist zuweisungskompatibel (§5.2) mit T und außerdem:

  • [...]
  • Wenn T ein Aufzählungstyp ist und V eine Aufzählungskonstante ist.

Und eine Aufzählungskonstante ist definiert als die tatsächliche Aufzählungskonstante ( JLS # 8.9.1 ), keine Variable, die auf diese Konstante zeigt.

Fazit: Wenn Sie eine Aufzählung als Parameter für Ihre Anmerkung verwenden möchten, müssen Sie ihr einen expliziten MyEnum.XXXXWert geben. Wenn Sie eine Variable verwenden möchten, müssen Sie einen anderen Typ auswählen (keine Aufzählung).

Eine mögliche Problemumgehung besteht darin, ein Stringoder zu verwenden int, das Sie dann Ihrer Aufzählung zuordnen können. Sie verlieren die Typensicherheit, aber die Fehler können zur Laufzeit (= während der Tests) leicht erkannt werden.

Assylien
quelle
3
Markieren Sie dies als die Antwort: Es kann nicht getan werden, sagt das JLS. Ich hoffte, dass es möglich war. Informationen zur Problemumgehung: @gap_j hat das Mapping versucht, ich habe es auch versucht. Es erwies sich jedoch als Herausforderung, andere Variationen des Fehlers "muss konstant sein" zu vermeiden, ohne Kopfschmerzen hinzuzufügen. Ich habe meine Frage bearbeitet, um zu zeigen, was ich letztendlich getan habe.
user1118312
113

"Alle Probleme in der Informatik können durch eine andere Indirektionsebene gelöst werden" --- David Wheeler

Hier ist es:

Aufzählungsklasse:

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

Personenklasse:

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 {
...
}
Ivan Hristov
quelle
1
scheint gut - statischer Import von Gender.Constants. * wäre noch ordentlicher
Magnus Smith
2
Könnten Sie bitte überprüfen, ob Sie auf Gender.Constants.MALE_VALUE verweisen? Um Ihre Frage zu beantworten, wird der Code mehrmals getestet.
Ivan Hristov
4
In dem Beispiel, das Sie geben, verweisen Sie auf die Werte der Aufzählung und nicht auf die Konstanten. Sie müssen eine Konstante angeben, die lose aus dem JLS übersetzt wurde. Weitere Informationen zum JLS finden Sie in den anderen Antworten hier.
Ivan Hristov
1
Um in einer @ RollAllowed-Notation zu verwenden, müssten Sie auf den Wert verweisen, NICHT auf die Aufzählung. Beispiel: @RolesAllowed ({Gender.Constants.MALE_VALUE}) Dies funktioniert nicht: @RolesAllowed ({Gender.MALE}) Sie können stattdessen auch eine Schnittstelle oder eine Klasse mit nur Konstanten verwenden.
Atom88
1
LOL, der genaue Anwendungsfall und die JSON-Anmerkung, für die ich dies benötigte. Seltsam.
15.
27

Ich denke, dass die am häufigsten gewählte Antwort unvollständig ist, da sie überhaupt nicht garantiert, dass der Aufzählungswert mit dem zugrunde liegenden konstanten StringWert gekoppelt ist . Mit dieser Lösung sollte man nur die beiden Klassen entkoppeln.

Stattdessen schlage ich eher vor, die in dieser Antwort gezeigte Kopplung zu verstärken, indem die Korrelation zwischen dem Namen der Aufzählung und dem konstanten Wert wie folgt erzwungen wird:

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

Wie von @GhostCat in einem Kommentar hervorgehoben, müssen geeignete Komponententests durchgeführt werden, um die Kopplung sicherzustellen.

Jean Valjean
quelle
3
Die vorherige Antwort kann nicht sinnlos sein, wenn Sie auf dieser Grundlage eine eigene Antwort erstellen.
Dantuch
Ja, genau. Das am besten geeignete Wort war "unvollständig".
JeanValjean
5
Vielen Dank an JeanValjean, es ist ein wertvoller Beitrag! (vom Autor der am meisten gewählten Antwort)
Ivan Hristov
2
Ich bin mir nicht sicher, ob das notwendig ist. Sie überprüfen bereits, ob die Enum-Konstante mit der Zeichenfolge übereinstimmt. Wenn Sie einen Tippfehler im Namen der Aufzählung und in der rohen Zeichenfolge haben, würde ein anderer Komponententest dort wirklich helfen?
GhostCat
1
Das überlasse ich dir. Vielleicht sollten Sie hinzufügen, dass ein Komponententest verwendet werden sollte, um sicherzustellen, dass die Überprüfung, die Sie dem Konstruktor hinzugefügt haben, vor dem Versand des Codes erfolgt. Es hilft nicht, diese Überprüfung zu haben, wenn es zum ersten Mal ausgeführt wird, wenn Ihr Kunde Ihre Java-Anwendung startet
;-)
7

Die Steuerregel scheint zu lauten: "Wenn T ein Aufzählungstyp ist und V eine Aufzählungskonstante.", 9.7.1. Normale Anmerkungen . Aus dem Text geht hervor, dass das JLS eine äußerst einfache Bewertung der Ausdrücke in Anmerkungen anstrebt. Eine Enum-Konstante ist speziell der Bezeichner, der in der Enum-Deklaration verwendet wird.

Selbst in anderen Kontexten scheint ein mit einer Enum-Konstante initialisiertes Finale kein konstanter Ausdruck zu sein. 4.12.4. final Variables sagt "Eine Variable vom primitiven Typ oder Typ String, die final ist und mit einem Konstantenausdruck zur Kompilierungszeit (§15.28) initialisiert wird, wird als konstante Variable bezeichnet.", enthält jedoch kein mit en initialisiertes Finale vom Typ enum Enum-Konstante.

Ich habe auch einen einfachen Fall getestet, in dem es darauf ankommt, ob ein Ausdruck ein konstanter Ausdruck ist - ein Fall, der eine Zuweisung zu einer nicht zugewiesenen Variablen umgibt. Die Variable wurde nicht zugewiesen. Eine alternative Version desselben Codes, der stattdessen ein endgültiges int getestet hat, hat die Variable definitiv zugewiesen:

  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
    }
  }
Patricia Shanahan
quelle
Ja, es sieht so aus, als ob "das JLS eine äußerst einfache Bewertung der Ausdrücke in Anmerkungen anstrebt". Über den Code bekomme ich eine "3", wenn ich ihn so wie er ist ausführe. Aus dem Text ging hervor, dass Sie mit MyEnum keine "3" und mit dem (auskommentierten) "z" eine 3 erhalten haben. Könntest Du das erläutern?
user1118312
Das ist interessant - es sieht so aus, als ob sich Compiler darin unterscheiden. Die auskommentierte Version sollte funktionieren, da (z == 3) mit za static final int auf der Liste der konstanten Ausdrücke steht. Ich werde es mit ein paar Compilern überprüfen und sehen, was ich herausfinden kann.
Patricia Shanahan
2

Ich zitiere aus der letzten Zeile der Frage

Jeder Weg, um diese Ziele zu erreichen, wäre in Ordnung.

Also habe ich es versucht

  1. Der Annotation wurde ein enumType-Parameter als Platzhalter hinzugefügt

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    public @interface MyAnnotation {
    
        String theString();
        int theInt();
        MyAnnotationEnum theEnum() default MyAnnotationEnum.APPLE;
        int theEnumType() default 1;
    }
    
  2. In der Implementierung wurde eine getType-Methode hinzugefügt

    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. Es wurde eine Änderung vorgenommen, um eine int-Konstante anstelle der Aufzählung zu verwenden

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

Ich leite die MYENUM-Konstante von MYENUM_TYPE int ab. Wenn Sie also MYENUM ändern, müssen Sie nur den int-Wert in den entsprechenden Wert für den Aufzählungstyp ändern.

Es ist nicht die eleganteste Lösung, aber ich gebe es wegen der letzten Zeile in der Frage.

Jeder Weg, um diese Ziele zu erreichen, wäre in Ordnung.

Nur eine Randnotiz, wenn Sie versuchen, zu verwenden

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

Der Compiler sagt bei der Annotation - MyAnnotation.theEnumType muss eine Konstante sein

Aditya
quelle
Beziehen Sie sich auch auf diese Antwort für eine ähnliche Frage
Aditya
1
Danke gap_j. Aber es wäre nicht genau typsicher, in dem Sinne, dass "MYENUM_TYPE" unzulässige Werte annehmen kann (dh 30) und der Compiler es nicht bemerkt. Ich denke auch, dass das gleiche ohne zusätzlichen Code erreicht werden könnte, indem man: public static final int MYENUM_INT_CONSTANT = 0; public static final MyEnum MYENUM_CONSTANT = MyEnum.values ​​() [MYENUM_INT_CONSTANT]; ... @MyAnnotation (theEnumSimulation = MYENUM_INT_CONSTANT, theInt = INT_CONSTANT, theString = STRING_CONSTANT) public void methodB () {...
user1118312
Ich glaube nicht, dass das Problem beim Kompilieren gelöst werden kann. Mit dem Ansatz, den Sie gegeben haben, wirft ein Laufzeitfehlerjava.lang.ExceptionInInitializerError Caused by: java.lang.ArrayIndexOutOfBoundsException: 2
Aditya
Hmmm. In Ihrem Stacktrace wird eine "2" erwähnt, und in dem von mir eingegebenen Beispiel gibt es keine "2". Mit der "0" im Beispiel und unter Verwendung der ursprünglichen Aufzählung (nicht der mit dem Konstruktor und den Methoden) verhält es sich wie Ihr Code. Es werden keine Ausnahmen geworfen. Ich habe @assylias Antwort als akzeptiert markiert und meine Frage mit dem bearbeitet, was ich letztendlich getan habe. Dies ist nur eine Art "sicher".
user1118312
-1

Meine Lösung war

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

Ich dachte, das wäre der sauberste Weg. Dies erfüllt einige Anforderungen:

  • Wenn Sie möchten, dass die Aufzählungen numerisch sind
  • Wenn Sie möchten, dass die Aufzählungen von einem anderen Typ sind
  • Der Compiler benachrichtigt Sie, wenn ein Refactor auf einen anderen Wert verweist
  • Sauberster Anwendungsfall (minus ein Zeichen): @Annotation(foo = MyEnum._FOO)

BEARBEITEN

Dies führt gelegentlich zu Kompilierungsfehlern, die zum Grund des Originals führen element value must be a constant expression

Das ist also anscheinend keine Option!

DKo
quelle