Annotation attributes with type parameters
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.
精彩评论