定义Java接口时,可以使用类型参数声明一个方法,例如:
public interface ExampleInterface {
<E extends Enum<E>> Class<E> options();
}
同样的事情在注释中不起作用。例如,这是非法的:
public @interface ExampleAnnotation {
<E extends Enum<E>> Class<E> options();
}
我可以使用原始类型Enum
:
public @interface ExampleAnnotation {
@SuppressWarnings("rawtypes")
Class<? extends Enum> options();
}
为什么无法使用类型参数声明注释属性到底是什么原因?
答案 0 :(得分:17)
我认为这是可能的,但它需要对语言规范进行大量补充,这是不合理的。
首先,对于enum示例,您可以使用Class<? extends Enum<?>> options
。
Class<? extends Enum> options
还有另一个问题:由于Enum.class
是一个Class<Enum>
Class<? extends Enum>
,因此options=Enum.class
Class<? extends Enum<?>> options
不会发生这种情况,因为Enum
不是Enum<?>
的子类型,在杂乱的原始类型处理中是一个相当偶然的事实。
回到一般问题。由于在有限的属性类型中,Class
是唯一具有类型参数的属性,并且通配符通常足够表达,因此您的关注点不值得解决。
让我们进一步概括问题,假设有更多属性类型,并且在许多情况下通配符不够强大。例如,假设允许Map
,例如
Map<String,Integer> options();
options={"a":1, "b":2} // suppose we have "map literal"
假设我们希望任何类型Map<x,x>
的attrbite类型为x
。这不能用通配符表示 - Map<?,?>
对Map<x,y>
来说意味着x,y
。
一种方法是允许类型的类型参数:<X>Map<X,X>
。这实际上非常有用。但这是打字系统的一个重大改变。
另一种方法是重新解释注释类型中方法的类型参数。
<X> Map<X,X> options();
options={ "a":"a", "b":"b" } // infer X=String
这在当前对方法类型参数,推理规则,继承规则等的理解中根本不起作用。我们需要更改/添加很多东西才能使它工作。
在任何一种方法中,如何将X
传递给注释处理器是一个问题。我们必须发明一些额外的机制来携带实例的类型参数。
答案 1 :(得分:11)
答案 2 :(得分:7)
Section 9.6描述了注释。其中一句话是:
如果在注释类型中声明的方法的返回类型是除以下之一之外的任何类型,则是编译时错误:其中一种基本类型,String,Class和任何类的调用,枚举类型(§8.9),注释类型或前述类型之一的数组(§10)。如果在注释类型中声明的任何方法具有覆盖等同于在类Object或接口annotation.Annotation中声明的任何公共或受保护方法的签名,则它也是编译时错误。
然后它说下面的内容,我认为这个问题的关键是:
请注意,这与泛型方法的禁止不冲突,因为通配符消除了对显式类型参数的需要。
所以它建议我应该使用通配符,并且不需要类型参数。为了摆脱原始类型Enum
,我只需要在其答案中使用Enum<?>
作为无可争议的建议:
public @interface ExampleAnnotation {
Class<? extends Enum<?>> options();
}
可能允许类型参数会打开一堆蠕虫,因此语言设计者决定简单地禁止它们,因为你可以通过通配符得到你需要的东西。
答案 3 :(得分:4)
他们想要引入注释,以便人们只能将它们用作,,,注释。并防止开发人员将逻辑放入其中。 即使用注释开始编程内容,这可能会使Java在我看来看起来像一个非常不同的语言。 因此,Java语言规范中的上下文无关语法注释。
对注释类型声明施加以下限制 凭借其上下文免费语法:
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)
为了更好地理解我的意思,看看这个JVM黑客的作用: http://www.cs.rice.edu/~mgricken/research/xajavac/
他创建 And,或注释作为指令并使用它们处理其他注释。无价!
答案 4 :(得分:2)
我承认在这里参加聚会很晚,但是我自己在这个确切的问题上苦苦挣扎了好一阵子,所以我想对此稍作补充。
注意:这是一个很长的答案,除非您对JVM的低级细节感兴趣或正在从事实现的工作,否则您可能不需要阅读它。 JVM之上的新编程语言。
首先,Java语言和作为其基础平台的Java虚拟机之间存在差异。 Java语言是Java语言规范的一部分,该规范已被一些人在其答案中引用。 JVM受Java虚拟机规范的约束,除Java外,它还支持其他几种编程语言,例如Scala,Ceylon,Xtend和Kotlin。 JVM充当所有这些语言的共同点,因此,它必须比基于JVM的语言更具宽容性。
现有答案中引用的限制是Java语言的限制,而不是JVM的限制。在大多数情况下,这些限制在JVM级别上并不存在。
例如,假设您要定义类似以下内容的内容(关于为什么的解释最后要说明):
@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
}
}
显然不可能用Java编写此代码,因为(a)它包含具有类型参数的注释,(b)因为该注释称为class
(小写的{{ 1}}),这是Java中的保留关键字。
但是,使用ByteBuddy之类的代码生成框架,确实可以通过编程方式创建相应的JVM字节码:
c
(以上代码是用Xtend语法编写的,但可以轻松转换为常规Java)
简而言之,这段代码将创建一个参数化注释(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"))]
}
}
)并将其用作另一个注释(@class<T>
)的属性,其中类型参数绑定到@before
。通过将? extends Runnable
替换为forEach[load(...)]
(以生成实际的类文件)并在同一文件夹中编译小型Java测试程序,可以轻松地验证生成的代码的有效性:
forEach[saveIn(...)]
测试程序将显示包装在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);
}
}
批注中的预期的初始化程序类:
@class
要更好地理解它可以实现(和不能实现),可以通过@class(value=class Initializer)
分解一些生成的类文件:
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;
的类型参数正确地反映在类和方法级别,并且也正确显示在T
属性的签名中。
value
对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)
注释的反汇编再次表明,具体类型参数(@before
)已正确记录在实际方法签名和? extends Runnable
属性中。
因此,如果您使用的语言能够进行参数化注释,则字节码将保留在编译时保证类型安全所需的所有信息。话虽这么说(即类型安全性主要在Java的编译时强制执行),但我认为JVM级别上没有任何东西可以防止将未扩展Signature
的类分配为默认值到Runnable
批注的code
属性中去(但同样,检测并阻止它是编译器的工作)。
最后,房间里的大问题是:为什么有人要做所有这些事情?
我实际上并没有从头开始编写所有这些代码,只是为已经回答的问题提供了一个晦涩的答案。我上面粘贴的代码来自(略作编辑的)基于JVM的编程语言的测试用例。这种语言的注释经常需要随代码一起携带代码(以对包含代码的类的引用的形式)。这是实现类似于Xtend编程语言中的active annotations功能的语言功能所必需的。现在,由于@before
是有效的注释属性类型,可以直接使用类文字来实现。但是,这将直接公开Java API,这是不理想的,因为它将创建紧密的耦合。如果将类文字包装在其他属性类型中,则它必须是另一个注释,并且如果我们不想在过程中丢失类型信息,则此注释需要具有类型参数以携带该信息。 / p>
因此,长话短说,参数化的注释是可能的(在JVM上,而不是在Java中),并且在某些用例中需要使用它们,但是实际上,这仅对JVM语言实现者有意义。
BTW,另一位张贴者谈到了“使用注释编程东西”在Java中并不是真正的预期功能,但我强烈建议您看一下Xtend的活动注释功能。就是这样,即“使用注释对东西进行编程”,一旦掌握了它,它就是一项非常强大的语言功能。