Перечислите значение по умолчанию для значения аннотации перечисления Java - PullRequest
20 голосов
/ 16 августа 2011

Java допускает enum в качестве значений для аннотаций. Как определить тип общего значения по умолчанию enum для значения аннотации enum?

Я рассмотрел следующее, но оно не скомпилируется:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public <T extends Enum<T>> @interface MyAnnotation<T> {

    T defaultValue();

}

Есть решение этой проблемы или нет?

BOUNTY

Не похоже, что есть прямое решение этого углового случая Java. Итак, я начинаю вознаграждение, чтобы найти самое элегантное решение этой проблемы.

Идеальное решение должно в идеале соответствовать следующим критериям:

  1. Одна аннотация, многократно используемая для всех перечислений
  2. Минимальные усилия / сложность для получения значения перечисления по умолчанию в виде перечисления из экземпляров аннотаций

лучшее решение так далеко

По дюнам:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {

    // By not specifying default,
    // we force the user to specify values
    Class<? extends Enum<?>> enumClazz();
    String defaultValue();

}

...

public enum MyEnumType {
    A, B, D, Q;
}

...

// Usage
@MyAnnotation(enumClazz=MyEnumType.class, defaultValue="A"); 
private MyEnumType myEnumField;

Конечно, мы не можем заставить пользователя указать допустимое значение по умолчанию во время компиляции. Однако любая предварительная обработка аннотации может подтвердить это с помощью valueOf().

УЛУЧШЕНИЕ

Arian предлагает элегантное решение для избавления от clazz в аннотированных полях:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {

}

...

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@MyAnnotation()
public @interface MyEnumAnnotation {

    MyEnumType value(); // no default has user define default value

}

...

@MyEnumAnnotation(MyEnum.FOO)
private MyEnumType myValue;

Обработчик аннотаций должен искать оба поля MyEnumAnnotation для заданного значения по умолчанию.

Это требует создания одного типа аннотации для каждого типа перечисления, но гарантирует безопасность проверенного типа во время компиляции.

Ответы [ 7 ]

5 голосов
/ 16 августа 2011

Не совсем уверен, что вы имеете в виду, когда говорите, получите значение по умолчанию, если указанное значение не было предоставлено в аргументах конструктора, но не заботитесь о универсальном типе во время выполнения.

Следующее работает, но немного уродливо.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class Main {

    @MyAnnotation(clazz = MyEnum.class, name = "A")
    private MyEnum value;

    public static v oid main(String[] args) {
        new Main().printValue();
    }

    public void printValue() {
        System.out.println(getValue());
    }

    public MyEnum getValue() {
        if (value == null) {
            value = getDefaultValue("value", MyEnum.class);
        }
        return value;
    }

    private <T extends Enum<?>> T getDefaultValue(String name, Class<T> clazz) {

        try {
            MyAnnotation annotation = Main.class.getDeclaredField(name)
                    .getAnnotation(MyAnnotation.class);

            Method valueOf = clazz.getMethod("valueOf", String.class);

            return clazz.cast(valueOf.invoke(this, annotation.value()));

        } catch (SecurityException e) {
            throw new IllegalStateException(e);
        } catch (NoSuchFieldException e) {
            throw new IllegalArgumentException(name, e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        } catch (NoSuchMethodException e) {
                throw new IllegalStateException(e);
        } catch (InvocationTargetException e) {
            if (e.getCause() instanceof RuntimeException) {
                throw (RuntimeException) e.getCause();
                /* rethrow original runtime exception 
                 * For instance, if value = "C" */
            }
            throw new IllegalStateException(e);
        }
    }

    public enum MyEnum {
        A, B;
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface MyAnnotation {

        Class<? extends Enum<?>> clazz();

        String name();
    }
}

edit: я изменил getDefaultValue, чтобы он работал с помощью метода перечислений valueOf, что дало лучшее сообщение об ошибке, если указанное значение не является ссылочным экземпляром перечисления.

3 голосов
/ 18 августа 2011

Фреймворки, использующие аннотации, могут реально выиграть от использования apt . Это препроцессор, содержащийся в javac , который позволит вам анализировать объявления и их аннотации (но не локальные объявления внутри методов).

Для вашей проблемы вам нужно написать AnnotationProcessor (класс, используемый в качестве начальной точки для предварительной обработки) для анализа аннотации с использованием Mirror API . На самом деле аннотация Dunes довольно близка к тому, что здесь необходимо. Жаль, что имена перечислений не являются константными выражениями, иначе решение Dunes было бы неплохо.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
    Class<? extends Enum<?>> clazz();
    String name() default "";
}

А вот пример перечисления: enum MyEnum { FOO, BAR, BAZ, ; }

При использовании современной IDE вы можете отображать ошибки непосредственно в элементе аннотации (или в значении аннотации), если имя не является допустимой константой перечисления. Вы даже можете предоставить подсказки для автозаполнения, поэтому, когда пользователь пишет @MyAnnotation(clazz = MyEnum.class, name = "B") и нажимает горячие клавиши для автоматического завершения после записи B , вы можете предоставить ему список на выбор, содержащий все константы, начинающиеся с B : БАР и БАЗ.

Мое предложение реализовать значение по умолчанию - создать аннотацию маркера, чтобы объявить константу перечисления как значение по умолчанию для этого перечисления. Пользователь по-прежнему должен будет указать тип перечисления, но может опустить имя. Однако есть и другие способы сделать значение значением по умолчанию.

Вот учебник о apt и вот AbstractProcessor , который должен быть расширен для переопределения метода getCompletions.

3 голосов
/ 16 августа 2011

Ваш синтаксис общего типа немного не в порядке.Должно быть:

public @interface MyAnnotation<T extends Enum<T>> {...

, но компилятор выдает ошибку:

Синтаксическая ошибка, объявление аннотации не может иметь параметры типа

Хорошая идея.Похоже, это не поддерживается.

3 голосов
/ 16 августа 2011

Проще говоря, вы не можете этого сделать.Перечисления не могут быть легко использованы в качестве универсальных типов;возможно, с одним исключением, которое заключается в том, что Enums может на самом деле реализовывать интерфейсы, которые допускают несколько динамическое использование.Но это не будет работать с аннотациями, так как набор типов, которые можно использовать, строго ограничен.

2 голосов
/ 23 августа 2011

Я не уверен, каков ваш вариант использования, поэтому у меня есть два ответа:

Ответ 1:

Если вы просто хотите написать как маленький коднасколько это возможно, вот мое предложение: Dunes ' answer:

public enum ImplicitType {
    DO_NOT_USE;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {

    Class<? extends Enum<?>> clazz() default ImplicitType.class;

    String value();
}

@MyAnnotation("A"); 
private MyEnumType myEnumField;

Когда clazz равно ImplicitType.class, используйте тип поля в качестве класса enum.

Ответ 2:

Если вы хотите поработать с фреймворком и сохранить безопасность типов, проверенных компилятором, вы можете сделать что-то вроде этого:

/** Marks annotation types that provide MyRelevantData */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface MyAnnotation {
}

И вкод клиента, у вас будет

/** Provides MyRelevantData for TheFramework */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@MyAnnotation
public @interface MyEnumAnnotation {

    MyEnumType value(); // default MyEnumType.FOO;

}

@MyEnumAnnotation(MyEnum.FOO)
private MyEnumType myValue;

. В этом случае вы будете сканировать поле на наличие аннотаций, которые снова помечены MyAnnotation.Однако вам придется получить доступ к значению через отражение в объекте аннотации.Похоже, что этот подход является более сложным на стороне структуры.

2 голосов
/ 20 августа 2011

Мое предложение похоже на предложение Капепа . Разница в том, что я предлагаю использовать процессор аннотаций для создания кода.

Простой пример: если вы намереваетесь использовать это только для перечислений, которые вы сами написали. Аннотируйте перечисление специальным перечислением. Затем процессор аннотаций создаст новую аннотацию только для этого перечисления.

Если вы работаете с большим количеством перечислений, которые вы не написали, вы можете реализовать некоторую схему отображения имен: enum name -> name of annotation name. Затем, когда обработчик аннотаций встретит одно из этих перечислений в вашем коде, он автоматически сгенерирует соответствующую аннотацию.

Вы просили:

  1. Одна аннотация, многократно используемая для всех перечислений ... технически нет, но я думаю, что эффект тот же.
  2. Минимальные усилия / сложность для получения значения перечисления по умолчанию в виде перечисления из экземпляров аннотаций ... вы можете получить значение перечисления по умолчанию без какой-либо специальной обработки
1 голос
/ 27 марта 2017

У меня была похожая потребность, и я нашел следующее довольно простое решение:

Фактический @Default интерфейс:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Default {}

Использование:

public enum Foo {
    A,
    @Default B,
    C;
}

Нахождение значения по умолчанию:

public abstract class EnumHelpers {
    public static <T extends Enum<?>> T defaultEnum(Class<T> clazz) {
        Map<String, T> byName = Arrays.asList(clazz.getEnumConstants()).stream()
            .collect(Collectors.toMap(ec -> ec.name(), ec -> ec));

        return Arrays.asList(clazz.getFields()).stream()
             .filter(f -> f.getAnnotation(Default.class) != null)
             .map(f -> byName.get(f.getName()))
             .findFirst()
             .orElse(clazz.getEnumConstants()[0]);
    }   
}

Я также поиграл с возвращением Optional<T> вместо значения по умолчанию для первой константы Enum, объявленной в классе.

Это, конечно, конечно, будьте объявлением по умолчанию для всего класса, но это соответствует тому, что мне нужно.YMMV:)

...