如何从 Java 中的常量为注释提供 Enum 值

我无法使用从常量获取的 Enum 作为注释中的参数。我得到了这个编译错误: “注释属性[属性]的值必须是枚举常量表达式”。

这是 Enum 代码的简化版本:

public enum MyEnum {
APPLE, ORANGE
}

注释:

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


int theInt();


MyEnum theEnum();
}

还有班级:

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


}


}

错误只显示在 methodB 上的“ theEnum = MYENUM _ CONSTANT”中。编译器可以使用 String 和 int 常量,但 Enum 常量不行,即使它的值与 methodA 上的值完全相同。在我看来,这似乎是编译器中缺少的一个特性,因为这三个特性显然都是常量。没有方法调用,没有对类的奇怪使用,等等。

我想实现的是:

  • 在注释和后面的代码中使用 MYENUM _ CONSTANT。
  • 保持类型安全。

任何实现这些目标的方法都是可行的。

编辑:

谢谢大家。就像你说的,这是不可能的。JLS 应该更新。这次我决定忘记注释中的枚举,而使用常规的 int 常量。只要 int 是从命名常量赋值的,值就是有界的,并且是“有点”安全的类型。

它看起来像这样:

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

我可以在代码中的任何其他地方使用 MYENUMSIMUL _ CONSTANT。

95349 次浏览

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

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

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

"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 {
...
}

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.

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!