97

I'm unable to use an Enum taken from a Constant as a parameter in an annotation. I get this compilation error: "The value for annotation attribute [attribute] must be an enum constant expression".

This is a simplified version of the code for the Enum:

public enum MyEnum {
    APPLE, ORANGE
}

For the Annotation:

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

    int theInt();

    MyEnum theEnum();
}

And the class:

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

    }

}

The error shows up only in "theEnum = MYENUM_CONSTANT" over methodB. String and int constants are ok with the compiler, the Enum constant is not, even though it's the exact same value as the one over methodA. Looks to me like this is a missing feature in the compiler, because all three are obviously constants. There are no method calls, no strange use of classes, etc.

What I want to achieve is:

  • To use the MYENUM_CONSTANT in both the annotation and later in the code.
  • To stay type safe.

Any way to achieve these goals would be fine.

Edit:

Thanks all. As you say, it cannot be done. The JLS should be updated. I decided to forget about enums in annotations this time, and use regular int constants. As long as the int is assigned from a named constant, the values are bounded and it's "sort of" type safe.

It looks like this:

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

And I can use MYENUMSIMUL_CONSTANT anywhere else in the code.

user1118312
  • 1,253
  • 1
  • 9
  • 11

6 Answers6

155

"All problems in computer science can be solved by another level of indirection" --- David Wheeler

Here it is:

Enum class:

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

Person class:

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
  • 3,046
  • 2
  • 25
  • 23
  • 1
    seems good - static import of Gender.Constants.* would be even neater – Magnus Smith Sep 12 '13 at 16:19
  • Doing it this way I still get an error message: 'The value for annotation attribute xxx.yyy must be a constant expression' – du-it Dec 15 '14 at 07:53
  • 2
    Could you please verify that you are referencing Gender.Constants.MALE_VALUE ? To answer your question - the code is tested on multiple occasions. – Ivan Hristov Dec 15 '14 at 09:45
  • Since it is too long, please have alook at http://stackoverflow.com/questions/27525746/assign-an-enum-element-to-an-annotation – du-it Dec 17 '14 at 12:33
  • 4
    In the example you give, you are referencing the enum's values and not the constants. You need to give a constant, this loosely translated from the JLS. Please, see the other answers here for further details about the JLS. – Ivan Hristov Dec 17 '14 at 12:44
  • This guy, he's a genius. #ProblemSolved. – arturvt Jul 05 '16 at 12:49
  • 1
    Actually in order to use in a @RolesAllowed notation you would have to reference the value, NOT the enum. Example: @RolesAllowed({ Gender.Constants.MALE_VALUE }) This does not work: @RolesAllowed({ Gender.MALE}) You might as well use an interface or a class with only constants instead. – atom88 Jan 19 '17 at 16:45
  • Hi @atom88, that is a very valuable contribution to the example. Would you like to complete it (maybe add one sentence including the annotation and the use case for the security JEE, etc.) or should I? Thanks! – Ivan Hristov Feb 17 '17 at 12:08
  • @IvanHristov, go ahead and add in an example if you'd like. – atom88 Jun 14 '17 at 18:58
  • 2
    LOL, the exact use case and json annotation I needed this for. Weird. – b15 Jul 19 '19 at 13:19
52

I think that the most voted answer is incomplete, since it does not guarantee at all that the enum value is coupled with the underlying constant String value. With that solution, one should just decouple the two classes.

Instead, I rather suggest to strengthen the coupling shown in that answer by enforcing the correlation between the enum name and the constant value as follows:

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

As pointed out by @GhostCat in a comment, proper unit tests must be put in place to ensure the coupling.

JeanValjean
  • 17,172
  • 23
  • 113
  • 157
  • 8
    The previous answer cannot be pointless, if you create your own answer basing on that one. – dantuch Feb 04 '17 at 13:27
  • Yeah right. The most appropriate word was "incomplete". – JeanValjean Feb 04 '17 at 13:32
  • 5
    Thank you @JeanValjean, it is a valuable contribution! (from the author of the most voted answer) – Ivan Hristov Feb 17 '17 at 12:10
  • Nice addendum. But *unit tests should be written checking that the strings are constants.* isnt necessary. The compiler will slap your fingers when you use a String that isn't a constant. Yes, even when declaring it as public static final, the computer will prevent you from doing `SOME_OTHER_CONSTANT.toString()`. – GhostCat Jun 04 '19 at 12:58
  • @GhostCat I meant that a unit test should assert that the defined string matches with the expected value. – JeanValjean Jun 04 '19 at 13:46
  • 2
    Not sure if that is necessary. You already check that the enum constant matches the string. If you have a typo in the enum name, and in the raw string, would another unit test really help there? – GhostCat Jun 04 '19 at 13:55
  • @GhostCat that's true. I think you are right! Would you like to update the answer? – JeanValjean Jun 04 '19 at 14:02
  • 1
    I leave that to you. Maybe you should add that a unit test should be used to *ensure* that the checking that you added to the constructor happens before shipping the code. It doesnt help to have that check when the first time it runs is when your customer starts your java application ;-) – GhostCat Jun 04 '19 at 14:09
25

It seems to be defined in the JLS #9.7.1:

[...] The type of V is assignment compatible (§5.2) with T, and furthermore:

  • [...]
  • If T is an enum type, and V is an enum constant.

And an enum constant is defined as the actual enum constant (JLS #8.9.1), not a variable that points to that constant.

Bottom line: if you want to use an enum as a parameter for your annotation, you will need to give it an explicit MyEnum.XXXX value. If you want to use a variable, you will need to pick another type (not an enum).

One possible workaround is to use a String or int that you can then map to your enum - you will loose the type safety but the errors can be spotted easily at runtime (= during tests).

Lii
  • 11,553
  • 8
  • 64
  • 88
assylias
  • 321,522
  • 82
  • 660
  • 783
  • 3
    Marking this one as the answer: it cannot be done, the JLS says so. I hoped it could be done. About the workaround: @gap_j tried mapping, I did try also. But avoiding other variations of the "must be a constant" error without adding headaches proved to be a challenge. I edited my question to show what I ended up doing. – user1118312 Nov 12 '12 at 15:51
  • Formally this may be a correct answer but no real solution. OTOH the answer in https://stackoverflow.com/questions/13253624/how-to-supply-enum-value-to-an-annotation-from-a-constant-in-java/42029962#42029962 proposes a compact and safe solution – Thomas Mahler Apr 26 '21 at 08:11
7

The controlling rule seems to be "If T is an enum type, and V is an enum constant.", 9.7.1. Normal Annotations. From the text, it appears the JLS is aiming for extremely simple evaluation of the expressions in annotations. An enum constant is specifically the identifier used inside the enum declaration.

Even in other contexts, a final initialized with an enum constant does not seem to be a constant expression. 4.12.4. final Variables says "A variable of primitive type or type String, that is final and initialized with a compile-time constant expression (§15.28), is called a constant variable.", but does not include a final of enum type initialized with an enum constant.

I also tested a simple case in which it matters whether an expression is a constant expression - an if surrounding an assignment to an unassigned variable. The variable did not become assigned. An alternative version of the same code that tested a final int instead did make the variable definitely assigned:

  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
  • 25,849
  • 4
  • 38
  • 75
  • 1
    Yes, it does look like "the JLS is aiming for extremely simple evaluation of the expressions in annotations". About the code, when I run it as-is I get a "3". It seemed from the text that you did not get a "3" with MyEnum and did get a 3 with the (commented out) "z". Can you clarify? – user1118312 Nov 12 '12 at 15:25
  • 1
    That is interesting - it looks as though compilers differ on this. The commented out version should work, because (z==3) with z a static final int is on the list of constant expressions. I'll check it with a few compilers and see what I can find out. – Patricia Shanahan Nov 12 '12 at 15:41
3

I quote from the last line in the question

Any way to achieve these goals would be fine.

So i tried this

  1. Added a enumType parameter to the annotation as a placeholder

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    public @interface MyAnnotation {
    
        String theString();
        int theInt();
        MyAnnotationEnum theEnum() default MyAnnotationEnum.APPLE;
        int theEnumType() default 1;
    }
    
  2. Added a getType method in the implementation

    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. Made a change to use an int constant instead of the enum

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

I derive the MYENUM constant from MYENUM_TYPE int, so if you change MYENUM you just need to change the int value to the corresponding enum type value.

Its not the most elegant solution, But i'm giving it because of the last line in the question.

Any way to achieve these goals would be fine.

Just a side note, if you try using

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

The compiler says at the annotation- MyAnnotation.theEnumType must be a constant

Aditya
  • 2,148
  • 3
  • 21
  • 34
  • Also refer to this [answer](http://stackoverflow.com/questions/8564854/error-setting-annotation-value-as-class-from-a-constant-why?rq=1) for a similar question – Aditya Nov 08 '12 at 11:53
  • 1
    Thanks gap_j. But it wouldn't be exactly type safe, in the sense that "MYENUM_TYPE" can take illegal values (i.e. 30) and the compiler wouldn't notice. I also think the same could be achieved with no additional code by doing: 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 Nov 12 '12 at 14:51
  • I don't think that problem can be solved at compile time. Using the approach you gave throws a runtime error `java.lang.ExceptionInInitializerError Caused by: java.lang.ArrayIndexOutOfBoundsException: 2` – Aditya Nov 13 '12 at 10:14
  • Hmmm. Your stacktrace mentions a "2", and there isn't a "2" in the sample I typed. With the "0" in the sample, and using the original enum (not the one with the constructor and methods) it behaves like your code. No exceptions are thrown. I marked @assylias answer as accepted and edited my question with what I ended up doing, which is only "sort of" type safe. – user1118312 Nov 13 '12 at 21:37
0

My solution was

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

I thought this was the cleanest way. This meets couple of requirements:

  • If you want the enums to be numeric
  • If you want the enums to be of some other type
  • Compiler notifies you if a refactor references a different value
  • Cleanest use-case (minus one character): @Annotation(foo = MyEnum._FOO)

EDIT

This leads occasionally to compilation error, which leads to the reason of the original element value must be a constant expression

So this is apparently not an option!

DKo
  • 820
  • 1
  • 9
  • 19