Як подати значення Enum до анотації від Constant у Java


77

Я не можу використовувати Enum, взятий з Constant, як параметр в анотації. Я отримую цю помилку компіляції: "Значення атрибута анотації [атрибут] має бути виразом константи переліку".

Це спрощена версія коду для 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() {

    }

}

Помилка відображається лише в "theEnum = MYENUM_CONSTANT" над методомB. Константи string і int - це нормально для компілятора, константа Enum - ні, хоча це точно таке ж значення, як і значення over 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 в будь-якому іншому місці коду.


Відповіді:


24

Здається, це визначено в JLS # 9.7.1 :

[...] Тип V сумісний з присвоєнням (§5.2) з T, і крім того:

  • [...]
  • Якщо T - тип переліку, а V - константа переліку.

І константа переліку визначається як фактична константа переліку ( JLS # 8.9.1 ), а не змінна, яка вказує на цю константу.

Підсумок: якщо ви хочете використовувати перелік як параметр для вашої анотації, вам потрібно буде вказати йому явне MyEnum.XXXXзначення. Якщо ви хочете використовувати змінну, вам потрібно буде вибрати інший тип (не перелік).

Одним із можливих обхідних шляхів є використання Stringабо того, intщо ви потім можете зіставити з вашим переліком - ви втратите безпеку типу, але помилки можна легко помітити під час виконання (= під час тестів).


3
Позначивши це як відповідь: цього зробити не можна, JLS говорить так. Я сподівався, що це можна зробити. Про обхідний шлях: @gap_j спробував зіставити, я теж спробував. Але уникнення інших варіацій помилки "повинно бути постійно" без додавання головних болів виявилося проблемою. Я відредагував своє запитання, щоб показати, що в підсумку робив.
user1118312

116

"Усі проблеми в галузі інформатики можна вирішити за допомогою іншого рівня опосередкованості" --- Девід Вілер

Ось:

Клас Enum:

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

Клас людини:

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

1
здається непоганим - статичний імпорт Gender.Contants. * був би ще акуратнішим
Магнус Сміт

2
Не могли б ви підтвердити, що ви посилаєтесь на Gender.Constants.MALE_VALUE? Щоб відповісти на ваше запитання - код тестується неодноразово.
Іван Христов

4
У наведеному прикладі ви посилаєтесь на значення переліку, а не на константи. Вам потрібно дати константу, це вільно перекладено з JLS. Будь ласка, див. Інші відповіді тут для отримання додаткової інформації про JLS.
Іван Христов

1
Насправді для того, щоб використовувати в позначенні @RolesAllowed, вам потрібно було б посилатися на значення, а НЕ на перелік. Приклад: @RolesAllowed ({Gender.Constants.MALE_VALUE}) Це не працює: @RolesAllowed ({Gender.MALE}) Ви можете замість цього використовувати інтерфейс або клас із лише константами.
atom88

1
LOL, точний варіант використання та анотація json, для якого мені це було потрібно. Дивно.
b15

28

Я думаю, що найбільш проголосована відповідь є неповною, оскільки вона взагалі не гарантує, що значення переліку поєднується з базовим постійним Stringзначенням . За допомогою цього рішення слід просто роз’єднати два класи.

Натомість я радше пропоную посилити зв'язок, показаний у цій відповіді, посилюючи співвідношення між іменем перерахування та значенням константи наступним чином:

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

Як зазначив @GhostCat у коментарі, для забезпечення зчеплення повинні бути проведені відповідні модульні тести.


3
Попередня відповідь не може бути безглуздою, якщо ви створите свою власну відповідь, спираючись на неї.
dantuch

Так звичайно. Найбільш підходящим словом було "неповне".
JeanValjean

5
Дякую @JeanValjean, це цінний внесок! (від автора найбільш голосованої відповіді)
Іван Христов

2
Не впевнений, що це необхідно. Ви вже перевірили, що константа enum відповідає рядку. Якщо у вас є друкарська помилка в імені перерахування та в необробленому рядку, чи справді допоможе інший модульний тест?
GhostCat

1
Я залишаю це вам. Можливо, вам слід додати, що слід використовувати модульний тест, щоб переконатися, що перевірка, яку ви додали до конструктора, відбувається перед відправкою коду. Не допомагає перевірка при першому запуску, коли ваш клієнт запускає вашу програму java ;-)
GhostCat

7

Здається, керуючим правилом є "Якщо T - тип переліку, а V - константа переліку.", 9.7.1. Звичайні анотації . З тексту виходить, що JLS націлений на надзвичайно просту оцінку виразів в анотаціях. Константа перерахування - це конкретно ідентифікатор, що використовується всередині оголошення переліку.

Навіть в інших контекстах фінал, ініціалізований константою переліку, здається, не є постійним виразом. 4.12.4. final Variables говорить "Змінна примітивного типу або типу String, яка є кінцевою та ініціалізована константним виразом часу компіляції (§15.28), називається константою змінної.", але не включає фінал типу enum, ініціалізований за допомогою константа переліку.

Я також перевірив простий випадок, коли важливо, чи є вираз постійним виразом - if, що оточує присвоєння неназначеній змінній. Змінній не призначено. Альтернативна версія того ж коду, який замість цього протестував остаточний int, зробила змінну точно визначеною:

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

Так, це виглядає як "JLS націлений на надзвичайно просту оцінку виразів в анотаціях". Щодо коду, коли я запускаю його як є, я отримую "3". З тексту здавалося, що ви не отримали "3" із MyEnum, а отримали 3 із (прокоментованим) "z". Ви можете пояснити?
user1118312

Це цікаво - схоже, компілятори розходяться в цьому. Закоментована версія повинна працювати, тому що (z == 3) із za static final int є у списку константних виразів. Я перевірю це у кількох компіляторів і побачу, що я можу дізнатись.
Patricia Shanahan

2

Цитую останній рядок у питанні

Будь-який спосіб досягнення цих цілей був би чудовим.

Тож я спробував це

  1. Додав параметр enumType до анотації як заповнювач

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    public @interface MyAnnotation {
    
        String theString();
        int theInt();
        MyAnnotationEnum theEnum() default MyAnnotationEnum.APPLE;
        int theEnumType() default 1;
    }
    
  2. Додано метод getType у реалізації

    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. Вніс зміни, використовуючи константу int замість переліку

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

Я отримую константу MYENUM з MYENUM_TYPE int, тому, якщо ви змінюєте MYENUM, вам просто потрібно змінити значення int на відповідне значення типу перерахування.

Це не найелегантніше рішення, але я даю його через останній рядок у питанні.

Будь-який спосіб досягнення цих цілей був би чудовим.

Просто допоміжна примітка, якщо ви спробуєте використати

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

Компілятор каже в анотації - MyAnnotation.theEnumType має бути константою


Також зверніться до цієї відповіді для подібного запитання
Адітя

1
Дякую gap_j. Але це не був би точно безпечний тип, в тому сенсі, що "MYENUM_TYPE" може приймати незаконні значення (тобто 30), і компілятор цього не помітить. Я також вважаю, що цього можна було досягти без додаткового коду, виконавши: public static final int MYENUM_INT_CONSTANT = 0; загальнодоступний статичний фінал MyEnum MYENUM_CONSTANT = MyEnum.values ​​() [MYENUM_INT_CONSTANT]; ... @MyAnnotation (theEnumSimulation = MYENUM_INT_CONSTANT, theInt = INT_CONSTANT, theString = STRING_CONSTANT) public void methodB () {...
user1118312

Я не думаю, що цю проблему можна вирішити під час компіляції. Використовуючи підхід, який ви дали, викидає помилку під час виконанняjava.lang.ExceptionInInitializerError Caused by: java.lang.ArrayIndexOutOfBoundsException: 2
Aditya

Мда. У вашій стек-трасі згадується "2", а в зразку, який я набрав, немає "2". З "0" у зразку та використовуючи оригінальний перелік (не той, що має конструктор та методи), він поводиться як ваш код. Ніяких винятків не кидають. Я позначив відповідь @assylias як прийняту і відредагував своє запитання тим, що в підсумку робив, що є безпечним лише для "типу".
user1118312

-1

Моє рішення було

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

Я думав, що це найчистіший спосіб. Це відповідає парі вимог:

  • Якщо ви хочете, щоб переліки були числовими
  • Якщо ви хочете, щоб переліки були якогось іншого типу
  • Компілятор повідомляє вас, якщо рефактор посилається на інше значення
  • Найчистіший варіант використання (мінус один символ): @Annotation(foo = MyEnum._FOO)

РЕДАГУВАТИ

Це іноді призводить до помилки компіляції, що призводить до причини оригіналу element value must be a constant expression

Тож це, мабуть, не варіант!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.