Monday, March 10, 2008

Java annotation parameters can't default to null

I don't update here often enough, but here's a tidbit I wish I'd found on Google earlier.

Suppose you want to write a Java annotation that has a parameter value, but you want its default value to be null. Well, too bad. It is illegal to write this annotation:

public @interface Optional {
   public String value() default null;
}

It's a compile time error. javac says "attribute value must be constant;" Eclipse says "The value for annotation attribute Optional.value must be a constant expression." In fact, it's not that surprising, because even if you didn't set a default value, writing this would also be illegal (same error):

@Optional(null)

What the error is saying is that you can't set a Java annotation parameter to null.

Why is this? The specifications are opaque. JSR-175, which defined annotations for Java 5, just says "If member type is a primitive type or String, the ConditionalExpression must be a constant expression (JLS 15.28)." JLS 15.28, in turn, says that constant expressions can be, for example, any of these:

true
(short)(1*2*3*4*5*6)
Integer.MAX_VALUE / 2
2.0 * Math.PI
"The integer " + Long.MAX_VALUE + " is mighty big."

Notice anything missing? That's right, null. You can never pass null as a Java annotation parameter value, because, uh, null isn't a ConstantExpression.

Why? We may never know. The only thing you can do is workaround it, like this:

public @interface Optional {
   public String value() default NULL;
   public static final NULL = "THIS IS A SPECIAL NULL VALUE - DO NOT USE";
}

... and then make your code carefully treat Optional.NULL as if it were really null.

LAME!

8 comments:

Anonymous said...

Even better:
------ start code ------------------------
public @interface MyAnnotation {

public String[] titles() default NULL;

public static final String NULL = "THIS IS A SPECIAL NULL VALUE - DO NOT USE";
}
------ end code ------------------------


Here we've got a String[] array parameter to the annotation, but the default value is set to a single constant String!

If you want to test for the default value for 'titles', you end up with code like this:


if (myAnno.titles()[0].equals(MyAnnotation.NULL)) {
...
}


Somehow the NULL String gets put into an array automatically. Very strange...

Anonymous said...

I think you mean:

public static final String NULL = ...

Vladimir Kovalyuk said...

The workaround exists for String but what if you'd like to make optional the following annotation:

@interface AttributeOverride{
String name();
Column column() default null;
ManyToOne manyToOne() default null;
}

where Column and ManyToOne are annotations as well

Vladimir Kovalyuk said...

I employ the workaround with arrays as follows:

interface @Foo {
boolean [] bar default {};
}

@Foo(bar=true)
class MyClass {
}

@Foo
class AnotherClass {
}

then I have to check whether the provided array has zero length:

void parseAnnotation(Foo foo) {
boolean bar = false; // just for instance
if (foo.bar().length == 0)
; // use default value
else if (foo.bar().length == 1)
bar = foo.bar()[0];
else
throw new IllegalStateException();
// work with bar
}

Unknown said...

This gets even funnier when your annotation type is not String. In my case I had a Class annotation, and I had to get around it by setting the interface to the interface itself. Java wouldn't even let me use a constant so the code can't even be shared from the outside. Plus, the class is one with type parameters, so it now generates unchecked warnings too. Excellent!

Anonymous said...

Hi!! Very interesting your article.

A question, How can I change the value of annotation dynamically, after compile-time? is it possible?

Thank you
Sofia

Johannes Schaback said...

Hi, I just figured out a way who to have kind of a null default value: use arrays.

For example

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation
{
String value();
int length() default -1;
String[] someValues() default {};
}


If you leave someValues={} empty, it serves as an equivalent to null, however you basically regard an empty array as null. The drawback of this approach is of course that people may put more than one value in the array which may semantically not intended.

I guess I write a little article about it on my blog to ponder the implications... :)

Johannes

Anonymous said...

An alternative approach that avoids the problem altogether is to use a second annotation for the parameter, e.g. instead of:

public @interface DebugOut {
  String prettyName();
}

You might do:

public @interface DebugOut {
}

public @interface DebugName {
  String prettyName();
}

And have your implementation check for @DebugName when processing @DebugOut.

The XStream third-party library uses a similar model in its API for optional annotations.