20

Java allows enum as values for annotation values. How can I define a kind of generic default enum value for an enum annotation value?

I have considered the following, but it won't compile:

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

    T defaultValue();

}

Is there a solution to this issue or not?

BOUNTY

Is does not seem like there is a direct solution to this Java corner case. So, I am starting a bounty to find the most elegant solution to this issue.

The ideal solution should ideally meet the following criteria:

  1. One annotation reusable on all enums
  2. Minimum effort/complexity to retrieve the default enum value as an enum from annotation instances

BEST SOLUTION SO FAR

By Dunes:

@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;

Of course, we can't force the user to specify a valid default value at compile time. However, any annotation pre-processing can verify this with valueOf().

IMPROVEMENT

Arian provides an elegant solution to get rid of clazz in annotated fields:

@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;

The annotation processor should search for both MyEnumAnnotation on fields for the provided default value.

This requires the creation of one annotation type per enum type, but guarantees compile time checked type safety.

10
  • Wouldn't that be kind of pointless? Whenever you would get to processing of the annotation at runtime the generic information would have been lost. Commented Aug 15, 2011 at 22:17
  • It would avoid me having to define one annotation per enum type for usage in my code (it is a compile time issue). Commented Aug 15, 2011 at 22:19
  • I didn't mean the idea isn't useful. But generics are only known at compile time. You've marked your annotation to be retained in the runtime. But to access the annotation you have to go via reflections -- you wouldn't have any idea what the generic type originally was. Commented Aug 15, 2011 at 22:33
  • Ok, but I am not interested in knowing the generic type at runtime. I just want to make sure I have a default value at hand when I initiate an object's enum field value if it is not provided in a constructor (for example). Commented Aug 15, 2011 at 22:37
  • What's your use case for declaring defaults using annotations, that you can't get using a simple private MyEnum field = MyEnum.DEFAULT;? Commented Aug 16, 2011 at 2:30

8 Answers 8

5
+50

Not entirely sure what you mean when you say get a default value if said value wasn't provided in the constructor args, but not be caring about the generic type at runtime.

The following works, but is a bit of an ugly hack though.

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: I changed the getDefaultValue to work via the valueOf method of enums, thus giving a better error message if the value given is not reference instance of the enum.

Sign up to request clarification or add additional context in comments.

1 Comment

"Not entirely sure what you mean when you say get a default value if said value wasn't provided in the constructor args" -> The default value should be defined in the annotation 'instance' over the annotated item.
4

I'm not sure what your use case is, so I have two answers:

Answer 1:

If you just want to write as little code as possible, here is my suggestion extending 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;

When clazz is ImplicitType.class, use the fields type as enum class.

Answer 2:

If you want to do some framework magic and want to maintain compiler checked type safety, you can do something like this:

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

And in the client code, you would have

/** 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;

In this case you would scan the field for annotations which again are annotated with MyAnnotation. You will have to access the value via reflection on the annotation object, though. Seems like this approach is more complex on the framework side.

2 Comments

Looks very interesting, but shouldn't clazz remain in your version of MyAnnotation and shouldn't MyEnumAnnotation be annotated with @MyAnnotation(clazz=MyEnumType.class)? Or am I missing something?
For the second approach, you don't need the clazz field, as you can get the enum value directly from the client annotation. If you should need the value type as well, you can use the return type of the value() function.
3

Simply put, you can not do that. Enums can not easily be used as generic types; with perhaps one exception, which is that Enums can actually implement interfaces which allows somewhat dynamic usage. But that won't work with annotations as set of types that can be used is strictly limited.

Comments

3

Your generic type syntax is a little off. It should be:

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

but compiler gives error:

Syntax error, annotation declaration cannot have type parameters

Nice idea. Looks like it's not supported.

Comments

3

Frameworks using annotations can really profit from using apt. It's a preprocesor contained in javac, which will let you analyse declarations and their annotations (but not local declarations inside methods).

For your problem you would need to write an AnnotationProcessor (a class used as starting point for preprocessing) to analyse the annotation by using the Mirror API. Actually Dunes' annotation is pretty close to what is needed here. Too bad enum names aren't constant expressions, otherwise Dunes' solution would be pretty nice.

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

And here's an example enum: enum MyEnum { FOO, BAR, BAZ, ; }

When using a modern IDE, you can display errors on directly on the annotation element (or the annotation's value), if the name isn't a valid enum constant. You can even provide auto complete hints, so when a user writes @MyAnnotation(clazz = MyEnum.class, name = "B") and hits the hotkeys for auto-completion after writing B, you can provide him a list to choose from, containing all the constants starting with B: BAR and BAZ.

My suggestion to implement the default value is to create a marker annotation to declare an enum constant as default value for that enum. The user would still need to provide the enum type, but could omit the name. There are probably other ways though, to make a value the default one.

Here's a tutorial about apt and here the AbstractProcessor which should be extended to override the getCompletions method.

2 Comments

Thanks. I know about annotation processor. I saw Dune's solution. I am just wondering whether someone has an even better idea. That's why I have set the bounty.
Checking the annotations once with a processor is better than always checking them at runtime. The generics you suggested would be nice but unfortunately aren't available. Enum names seem to be the only way to solve the problem (unless you use something else instead of enums), but they aren't compile time constant and therefore have to be written as compile time constant strings. Validating those strings at compile time would be quite convenient for the user.
2

My suggestion is similar to kapep's suggestion. The difference is that I propose using the annotation processor for code creation.

A simple example would be if you intended to use this only for enums that you yourself wrote. Annotate the enum with a special enum. The annotation processor will then generate a new annotation just for that enum.

If you work with a lot of enums that you did not write, then you could implement some name mapping scheme: enum name -> annotation name. Then when the annotation processor encountered one of these enums in your code, it would generate the appropriate annotation automatically.

You asked for:

  1. One annotation reusable on all enums ... technically no, but I think the effect is the same.
  2. Minimum effort/complexity to retrieve the default enum value as an enum from annotation instances ... you can retrieve the default enum value without any special processing

Comments

2

I had a similar need and came up with the following pretty straightforward solution:

The actual @Default interface:

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

Usage:

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

Finding the default:

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

I've also played around with returning an Optional<T> instead of defaulting to the first Enum constant declared in the class.

This would, of course, be a class-wide default declaration, but that matches what I need. YMMV :)

Comments

0

You can also use the annotation: @JsonEnumDefaultValue: JavaDocs

For example:

public enum MyEnumType {
    A,
    B,
    C,
    @JsonEnumDefaultValue DEFAULT
}

The only catch for this is that you need to add the deserializer property as well to the mapper;

ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.