开发者

Annotation attributes with type parameters

开发者 https://www.devze.com 2023-04-09 11:13 出处:网络
When you define a Java interface, it\'s possible to declare a method with type parameters, for example like this:

When you define a Java interface, it's possible to declare a method with type parameters, for example like this:

public interface ExampleInterface {
    <E extends Enum<E>> Class<E> options();
}

The same t开发者_如何学Gohing does not work in an annotation. This, for example, is illegal:

public @interface ExampleAnnotation {
    <E extends Enum<E>> Class<E> options();
}

I can get what I'm after by using the raw type Enum:

public @interface ExampleAnnotation {
    @SuppressWarnings("rawtypes")
    Class<? extends Enum> options();
}

What exactly is the reason why it is not possible to declare annotation attributes with type parameters?


I think it is possible, but it requires lots of additions to language spec, which is not justified.

First, for you enum example, you could use Class<? extends Enum<?>> options.

There is another problem in Class<? extends Enum> options: since Enum.class is a Class<Enum> which is a Class<? extends Enum>, it's legal to options=Enum.class

That can't happen with Class<? extends Enum<?>> options, because Enum is not a subtype of Enum<?>, a rather accidental fact in the messy raw type treatments.

Back to the general problem. Since among limited attribute types, Class is the only one with a type parameter, and wildcard usually is expressive enough, your concern isn't very much worth addressing.

Let's generalize the problem even further, suppose there are more attribute types, and wildcard isn't powerful enough in many cases. For example, let's say Map is allowed, e.g.

Map<String,Integer> options();

options={"a":1, "b":2} // suppose we have "map literal"

Suppose we want an attrbite type to be Map<x,x> for any type x. That can't be expressed with wildcards - Map<?,?> means rather Map<x,y> for any x,y.

One approach is to allow type parameters for a type: <X>Map<X,X>. This is actually quite useful in general. But it's a major change to type system.

Another approach is to reinterpret type parameters for methods in an annotation type.

<X> Map<X,X> options();

options={ "a":"a", "b":"b" }  // infer X=String

this doesn't work at all in the current understanding of method type parameters, inference rules, inheritance rules etc. We need to change/add a lot of things to make it work.

In either approaches, it's a problem how to deliver X to annotation processors. We'll have to invent some additional mechanism to carry type arguments with instances.


The The Java™ Language Specification Third Edition says:

The following restrictions are imposed on annotation type declarations by virtue of their context free syntax:

  • Annotation type declarations cannot be generic.
  • No extends clause is permitted. (Annotation types implicitly extend annotation.Annotation.)
  • Methods cannot have any parameters
  • Methods cannot have any type parameters
  • Method declarations cannot have a throws clause


Section 9.6 of the Java Language Specification describes annotations. One of the sentences there reads:

It is a compile-time error if the return type of a method declared in an annotation type is any type other than one of the following: one of the primitive types, String, Class and any invocation of Class, an enum type (§8.9), an annotation type, or an array (§10) of one of the preceding types. It is also a compile-time error if any method declared in an annotation type has a signature that is override-equivalent to that of any public or protected method declared in class Object or in the interface annotation.Annotation.

And then it says the following, which is I think the key to this problem:

Note that this does not conflict with the prohibition on generic methods, as wildcards eliminate the need for an explicit type parameter.

So it suggests that I should use wildcards and that type parameters are not necessary. To get rid of the raw type Enum, I just have to use Enum<?> as irreputable suggested in his answer:

public @interface ExampleAnnotation {
    Class<? extends Enum<?>> options();
}

Probably allowing type parameters would have opened up a can of worms, so that the language designers decided to simply disallow them, since you can get what you need with wildcards.


They wanted to introduce annotations in order for people only to use them as ,,,well annotations. And prevent developers from putting logic in them. i.e. start programming stuff using annotations, which might have an effect of making Java look like a very different language in my opinion. Hence the context free syntax note in Java Language Specification.

The following restrictions are imposed on annotation type declarations by virtue of their context free syntax:

Annotation type declarations cannot be generic.
No extends clause is permitted. (Annotation types implicitly extend annotation.Annotation.)
Methods cannot have any parameters
Methods cannot have any type parameters

(http://java.sun.com/docs/books/jls/third_edition/html/interfaces.html)

To better understand what I mean, look at what this JVM hacker does: http://ricken.us/research/xajavac/

He creates And, Or annotations as instructions and processes other annotations using them. Priceless!


I'm admittedly late to the party here, but having struggled with this exact question for a good while myself, I wanted to add a slightly different take on it.

NOTE: this is a pretty long answer, and you probably don't need to read it unless you are interested in low-level details of the JVM or if you are in the business of implementing new programming languages on top of the JVM.

First of all, there is a difference between Java the language and the Java Virtual Machine as its underlying platform. Java, the language, is governed by the Java Language Specification that several folks already cited in their answers. The JVM is governed by the Java Virtual Machine Specification, and, besides Java, it supports several other programming languages such as Scala, Ceylon, Xtend and Kotlin. The JVM acts as a common denominator for all these languages, and, as a result, it has to be a lot more permissive than the languages that are based on it.

The restrictions that were cited in existing answers are restrictions of the Java language, not of the JVM. For the most part, these restrictions do not exist at the JVM level.

For example, let's say you wanted to define something like the following (explanation as to why one would want to do this follows at the end):

@Retention(RUNTIME)
public @interface before
{
  class<? extends Runnable> code() default @class(Initializer.class);
}

public @interface class<T>
{
  Class<T> value();
}

public class Initializer extends Runnable
{
  @Override
  public void run()
  {
    // initialization code
  }
}

It is clearly not possible to write this code in Java, because (a) it involves an annotation that has a type parameter, and (b) because that annotation is called class (with lower-case c), which is a reserved keyword in Java.

However, using code generation frameworks like ByteBuddy, it is indeed possible to create the corresponding JVM bytecode programmatically:

import java.lang.annotation.Annotation
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import net.bytebuddy.ByteBuddy
import net.bytebuddy.description.annotation.AnnotationDescription
import net.bytebuddy.description.annotation.AnnotationValue
import net.bytebuddy.description.modifier.Visibility
import net.bytebuddy.description.type.TypeDefinition
import net.bytebuddy.description.type.TypeDescription
import net.bytebuddy.description.type.TypeDescription.Generic
import net.bytebuddy.dynamic.DynamicType.Unloaded
import net.bytebuddy.dynamic.scaffold.TypeValidation
import net.bytebuddy.implementation.StubMethod
import static java.lang.annotation.RetentionPolicy.RUNTIME
import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType
import static net.bytebuddy.description.type.TypeDescription.Generic.OfWildcardType.Latent.boundedAbove
import static net.bytebuddy.description.type.TypeDescription.CLASS
import static net.bytebuddy.matcher.ElementMatchers.named

class AnnotationWithTypeParameter
{
    def void createAnnotationWithTypeParameter()
    {
        val ByteBuddy codeGenerator = new ByteBuddy().with(TypeValidation.DISABLED)
        val TypeDefinition T = TypeDescription.Generic.Builder.typeVariable("T").build
        val TypeDefinition classT = TypeDescription.Generic.Builder.parameterizedType(CLASS, T).build
        val Unloaded<? extends Annotation> unloadedAnnotation = codeGenerator
            .makeAnnotation
            .merge(Visibility.PUBLIC)
            .name("class")
            .typeVariable("T")
            .defineMethod("value", classT, Visibility.PUBLIC)
            .withoutCode
            .make
        val TypeDescription classAnnotation = unloadedAnnotation.typeDescription
        val Unloaded<Runnable> unloadedRunnable = codeGenerator
            .subclass(Runnable).merge(Visibility.PUBLIC).name("Initializer")
            .method(named("run")).intercept(StubMethod.INSTANCE)
            .make
        val TypeDescription typeInitializer = unloadedRunnable.typeDescription
        val AnnotationDescription.Builder a = AnnotationDescription.Builder.ofType(classAnnotation)
            .define("value", typeInitializer)
        val AnnotationValue<?, ?> annotationValue = new AnnotationValue.ForAnnotationDescription(a.build)
        val TypeDescription classRunnable = new TypeDescription.ForLoadedType(Runnable)
        val Generic.Builder classExtendsRunnable = parameterizedType(classAnnotation, boundedAbove(classRunnable.asGenericType, classRunnable.asGenericType))
        val Retention runtimeRetention = new Retention()
        {
            override Class<Retention> annotationType() {Retention}
            override RetentionPolicy value() {RUNTIME}
        }
        val Unloaded<? extends Annotation> unloadedBefore = codeGenerator
            .makeAnnotation
            .merge(Visibility.PUBLIC)
            .name("before")
            .annotateType(runtimeRetention)
            .defineMethod("code", classExtendsRunnable.build, Visibility.PUBLIC)
            .defaultValue(annotationValue)
            .make
        #[unloadedBefore, unloadedAnnotation, unloadedRunnable].forEach[load(class.classLoader).loaded]
        //                 ...or alternatively something like: .forEach[saveIn(new File("/tmp"))]
    }
}

(the above code is written in Xtend syntax, but can easily be transformed into regular Java)

In a nutshell, this code will create a parameterized annotation (@class<T>) and use it as an attribute of another annotation (@before), where the type parameter is bound to ? extends Runnable. The validity of the generated code can be easily verified by replacing the forEach[load(...)] with a forEach[saveIn(...)] (to generate actual class files) and compiling a small Java test program in the same folder:

import java.lang.reflect.Method;
import java.lang.annotation.Annotation;

public class TestAnnotation
{
  @before
  public static void main(String[] arg) throws Exception
  {
    Method main = TestAnnotation.class.getDeclaredMethod("main", String[].class);
    @SuppressWarnings("unchecked")
    Class<? extends Annotation> beforeAnnotation = (Class<? extends Annotation>)Class.forName("before");
    Annotation before = main.getAnnotation(beforeAnnotation);
    Method code = before.getClass().getDeclaredMethod("code");
    Object classAnnotation = code.invoke(before);
    System.err.println(classAnnotation);
  }
}

The test program will show the expected initializer class wrapped in an @class annotation:

@class(value=class Initializer)

To get a better understanding what this achieves (and doesn't achieve) it's useful to disassemble some of the generated class files via javap -c -v:

Classfile /private/tmp/class.class
  Last modified Feb 28, 2020; size 265 bytes
  MD5 checksum f57e09ce9d174a6943f7b09704cbdea3
public interface class<T extends java.lang.Object> extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Utf8               class
   #2 = Class              #1             // class
   #3 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/annotation/Annotation;
   #4 = Utf8               java/lang/Object
   #5 = Class              #4             // java/lang/Object
   #6 = Utf8               java/lang/annotation/Annotation
   #7 = Class              #6             // java/lang/annotation/Annotation
   #8 = Utf8               value
   #9 = Utf8               ()Ljava/lang/Class;
  #10 = Utf8               ()Ljava/lang/Class<TT;>;
  #11 = Utf8               Signature
{
  public abstract java.lang.Class<T> value();
    descriptor: ()Ljava/lang/Class;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #10                          // ()Ljava/lang/Class<TT;>;
}
Signature: #3                           // <T:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/annotation/Annotation;

The above code shows that the type parameter T is properly reflected at the class and the method level and that is also shows up correctly in the value attribute's signature.

Classfile /private/tmp/before.class
  Last modified Feb 28, 2020; size 382 bytes
  MD5 checksum d2166167cf2adb8989a77dd320f9f44b
public interface before extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Utf8               before
   #2 = Class              #1             // before
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               java/lang/annotation/Annotation
   #6 = Class              #5             // java/lang/annotation/Annotation
   #7 = Utf8               Ljava/lang/annotation/Retention;
   #8 = Utf8               value
   #9 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #10 = Utf8               RUNTIME
  #11 = Utf8               code
  #12 = Utf8               ()Lclass;
  #13 = Utf8               ()Lclass<+Ljava/lang/Runnable;>;
  #14 = Utf8               Lclass;
  #15 = Utf8               LInitializer;
  #16 = Utf8               Signature
  #17 = Utf8               AnnotationDefault
  #18 = Utf8               RuntimeVisibleAnnotations
{
  public abstract class<? extends java.lang.Runnable> code();
    descriptor: ()Lclass;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #13                          // ()Lclass<+Ljava/lang/Runnable;>;
    AnnotationDefault:
      default_value: @#14(#8=c#15)}
RuntimeVisibleAnnotations:
  0: #7(#8=e#9.#10)

The disassembly for the @before annotation again shows that the concrete type argument (? extends Runnable) is properly recorded in both the actual method signature as well as the Signature attribute.

So, if you have a language that is capable of parameterized annotations, then the bytecode preserves all the information that you would need to guarantee type safety at compile time. That being said (i.e. type safety being mainly enforced at compile time in Java), I do not believe that there is anything at the JVM level that would prevent a class that does not extend Runnable from being assigned as a default value to the code attribute of the @before annotation (but again, it's the compiler's job to detect and prevent that).

Finally, the big question in the room is: why would anybody want to do all this? I didn't actually write all this code from scratch, just to provide an obscure answer to an already answered question. The code that I pasted above comes from (slightly redacted) test cases for a JVM-based programming language. Annotations in this language frequently need to carry code with them (in the form of references to classes that contain the code). This is a necessity for implementing language features similar to the active annotations feature in the Xtend programming language. Now, since java.lang.Class is a valid annotation attribute type this could just be achieved by using class literals directly. However, that would directly expose Java API, which is not desirable because it would create a tight coupling. If the class literal is to be wrapped in some other attribute type it has to be another annotation, and if we don't want to lose the type information in the process then this annotation needs to have a type parameter to carry that information.

So, long story short, parameterized annotations are possible (on the JVM, not in Java), and there are use cases where you need them, but in practice this will only be of interest for JVM language implementors.

BTW, another poster talked about how "programming stuff using annotations" is not really an intended feature in Java, but I highly recommend a look at Xtend's active annotation feature. It's exactly that, i.e. "programming stuff using annotations", and once you get the hang of it it is a very powerful language feature.

0

精彩评论

暂无评论...
验证码 换一张
取 消