注释嵌套注释
抽象
Java 5中的注释提供了非常强大的元数据机制。 但是,像其他任何东西一样,我们需要弄清楚在哪里使用它才有意义。 在本文中,我们将了解注释为何重要,并讨论使用和滥用案例。
表达元数据
让我们从我们熟悉的优秀Java程序员开始。 我们想表达一个类的实例可以被序列化。 我们可以通过实现Serializable接口来说明这一点,如下所示:
public class MyClassimplements java.io.Serializable
{
}
Serializable接口有几种方法? 答案当然是零。 现在,为什么我们要有一个带有零个方法的接口并从中继承呢? 我称它为继承锤 。 在这种情况下,我们没有使用继承来派生任何行为,也没有表达特定的契约,但要表示我们同意,如果该类的用户愿意,可以序列化该类的对象。 我们将Serializable接口称为标记接口。 其他接口(例如Cloneable)也属于标记接口的传统。 现在,如果我的对象中有一个字段而又不想序列化该怎么办? Java使用transient关键字来表达这一点,如下所示:
public class MyClass implements java.io.Serializable
{
private int val1;
private transient int val2;
}
因此,在上面的示例中,我们需要一个接口(可序列化)和一个关键字(瞬态)来完成我们的工作。
让我们继续这个例子。 假设我有一个提供某些服务的框架。 您可以将您的类的对象发送到我的框架。 但是,我需要知道您的对象是否是线程安全的,毕竟如果它不是线程安全的,您就不希望我从多个线程中并发使用它。 继续从上面的示例中学到的知识,我可以定义一个标记接口(我称其为ThreadSafe )。 如果实现此接口,则可以确定您的类是线程安全的。
public class MyClass
implements java.io.Serializable,VenkatsFramework.ThreadSafe
{
private int val1;
private transient int val2;
}
看到那很简单! 现在,假设您有一个此类的方法,无论出于何种原因,都不应该从多个线程中调用该方法。 我们该怎么做? 没问题。 我们可以请求将一个新的关键字引入Java语言,因此我们可以使用new关键字标记我们的方法(或者可以说我们可以使用synced关键字,但是您可以看到这种使用a的方法的局限性关键词)。
public class MyClass
implements java.io.Serializable, VenkatsFramework.ThreadSafe
{
private int val1;
private transient int val2;
public our_new_fangled_keyword void foo() // Not valid Java code
{
//...
}
}
如您所见,我们缺乏扩展元数据的表达能力。 可以说,我们要做的就是给类,方法和字段上色,以使其可序列化,线程安全或根据需要表达的内容。
给您更多动力
输入注释。 注释为我们提供了一种使用新的元数据扩展Java语言的方法。 它提供了强大的表达能力。 让我们看看如何使用注释优雅地表达上一个示例中描述的概念。
//ThreadSafe.java
package VenkatsFramework;
public @interface ThreadSafe
{
}
//NotThreadSafe.java
package VenkatsFramework;
public @interface NotThreadSafe
{
}
//MyClass.java
package com.agiledeveloper;
import VenkatsFramework.ThreadSafe;
import VenkatsFramework.NotThreadSafe;
@ThreadSafe
public class MyClass implements java.io.Serializable
{
private int val1;
private transient int val2;
@NotThreadSafe public void foo()
{
//...
}
}
注释ThreadSafe的编写就好像它是一个接口一样(稍后将对此进行详细介绍)。 在MyClass中 ,我使用了注释ThreadSafe (用于类)和NotThreadSafe (用于方法)。 使用框架时,我们通常会使用注释而不是定义它们。 但是,学习如何定义注释很有趣。
定义注释
让我们定义一个称为AuthorInfo的注释,该注释可用于表示谁编写了一段代码。 这是代码:
package com.agiledeveloper;
import java.lang.annotation.*;@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
public @interface AuthorInfo
{
public String name();
public String email() default "";
public String comment() default "";
}
AuthorInfo批注包含三个元素:名称,电子邮件和注释。 这些是使用类似语法的方法声明定义的。 电子邮件和评论具有默认值,因此当使用@AuthorInfo批注时,这两个可能会被忽略。 如果注释只有一个值,则可以指定该值而没有成员名称。
您可以在哪里(方法,类,字段等)使用@AuthorInfo批注? 这是使用meta-annotation @Target定义的(很好:使用自己的狗食的注释)。 @Target批注的可能ElementType值是:ANNOTATION_TYPE,CONSTRUCTOR,FIELD,LOCAL_VARIABLE,METHOD,PACKAGE,PARAMETER和TYPE。 @Inherited元注释表示注释不仅会影响声明注释的类,还会影响从声明类派生的任何类(对于AuthorInfo注释,使用Inherited作为作者确实没有意义。类的名称可能不同于其基类的作者)。
最后, @ Retention元注释会告诉我们注释将达到多远。 如果该值为RetentionPolicy.SOURCE,则注释在源代码中可见,但被编译器丢弃。 RetentionPolicy.CLASS的值表示注释信息将保留在类文件中,但不会加载到虚拟机中。 RetentionPolicy.RUNTIME的值表示该值将在运行时保留,并且您可以使用反射来浏览批注详细信息。 @Documented这里没有显示另外一个元注释,它表示注释的使用可以在javadoc中进行记录。
使用注释
这是使用AuthorInfo批注的示例:
package com.agiledeveloper;@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
public class SomeClass
{
@com.agiledeveloper.AuthorInfo(
name = "Venkat Subramaniam", comment = "bug free")
public void foo()
{
}
}
SomeClass使用名称元素的值指定@AuthorInfo批注。 同样,foo()方法具有@AuthorInfo批注。
以下示例无效:
package com.agiledeveloper;
@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
public class SomeClass2
{// ERROR 'com.agiledeveloper.AuthorInfo' not applicable to fields
@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
// Not valid
private int val1;
}
由于@AuthorInfo仅可用于类和方法(由@Target元注释定义),因此编译器会给出错误消息。
探索注释
如前所述,大多数时候我们将使用批注而不是定义批注。 但是,如果您好奇框架如何使用注释,就可以了。 使用反射,您可以探索类,方法,字段等上注释的详细信息。下面的示例代码展示了类的@AuthorInfo :
package com.agiledeveloper;
import java.lang.reflect.Method;
public class Example
{
public static void report(Class theClass)
{
if (theClass.isAnnotationPresent(AuthorInfo.class))
{
AuthorInfo authInfo =
(AuthorInfo) theClass.getAnnotation(AuthorInfo.class);
System.out.println(theClass.getName() +
" has @AuthorInfo annotation:");
System.out.printf("name = %s, email = %s, comment = %s\n",
authInfo.name(),
authInfo.email(), authInfo.comment());
}
else
{
System.out.println(theClass.getName()
+ " doesn't have @AuthorInfo annotation");
}
System.out.println("-----------------");
}
public static void main(String[] args)
{
report(SomeClass.class);
report(String.class);
}
}
上面程序的输出如下所示:
com.agiledeveloper.SomeClass has @AuthorInfo annotation:
name = Venkat Subramaniam, email = , comment =
-----------------
java.lang.String doesn't have @AuthorInfo annotation
-----------------
Class的isAnnotationPresent()方法告诉我们该类是否具有预期的Annotation。 您可以使用getAnnotation()方法获取Annotation详细信息。
注释示例
让我们看一下内置在Java中的注释。
package com.agiledeveloper;
public class POJOClass
{
/**
* @deprecated Replaced by someOtherFoo()...
*/
public static void foo()
{
}
@Deprecated public static void foo1()
{
}
}
foo()方法使用传统方法将一个方法声明为已弃用。 这种方法缺乏表现力,即使Sun编译器通常会在使用声明为已弃用的方法时发出警告,也无法保证所有编译器都会对此发出警告。 更加标准化和可移植的版本是使用@Deprecated注解(尽管它缺乏提供过时方法允许过时的描述的能力,因此您通常将其与旧方法结合使用),如下所示:
/**
* @deprecated Replaced by someOtherFoo1()...
*/
@Deprecated public static void foo1()
{
}
注释和锤子
俗话说:“如果仅有的工具是锤子,那么一切就好像钉子一样。” 尽管注释是一个很好的工具,但并非每种情况都保证它们的使用。 我们大多数人都不喜欢XML配置。 但是,突然之间,XML配置中的所有内容都不应成为注释。
对于要在代码中内部表达的内容,请使用注释。 例如,Tapestry中的@Persist注释就是一个很好的例子。 您想将bean的属性声明为持久性,并且Tapestry会负责将其存储(例如在会话中)。 我宁愿将此定义为注解,也不愿使用冗长的配置说同样的话。 如果我决定不使该属性持久化,那么无论如何代码都有很大的变化。
考虑另一个好的示例,在定义Web服务时,您如何描述需要将类的哪些方法作为Web服务方法公开,以便其描述可以出现在WSDL中? 我们遇到了为此使用配置文件的解决方案。 配置文件方法的一个问题是,如果您修改代码(例如更改方法名称),则还必须修改配置文件。 此外,您很少真正将方法作为服务方法来回配置。 使用注释将方法标记为Web服务方法可能很有意义。
注释的表达能力及其扩展语言元数据的能力使代码生成工具可以根据您所表达的特征为您创建代码。 注释也可以帮助我们表达某些方面。
现在考虑使用注释为方法配置安全设置。 那将是一个拉伸。 您可能会在部署期间修改安全设置。 您可能希望能够更改它,而不必修改代码并重新编译。 注释不是表达一些外在的东西并在代码外部更好地表达的东西的最佳选择。 这是否意味着我们需要为此使用广泛的XML配置? 不一定,正如我们在下一节中讨论的那样。
约定优于配置
某些内容更适合使用注释进行配置和表达。 某些内容更适合于在外部表达并与代码分开,可以采用XML,YAML等格式。但是,并非应配置所有内容。 配置提供了灵活性。 就像现实生活中一样,太多的事情都是不好的。 在某些情况下,可以根据约定而不是配置在应用程序中轻松确定某些内容。
例如,在JUnit 4.0之前的版本中,您可以通过在方法前面加上test来表明该方法是一种测试方法。 在JUnit 4.0中,您改为使用@Test批注标记方法。 不关注JUnit 4.0的其他功能和优势,这样更好吗? 您可能会指出,使用批注带来的好处是您不必从TestCase类扩展测试类。 我同意,但是那是在课堂上。 在方法级别上,简单地将方法编写为以下内容是否会减少噪音,减少混乱:
public void testMethod1() {}
or
@Test public void method1() {}
在默认情况下,为什么不将测试类中的所有方法都视为测试方法? 然后,您可以指定(也许使用注释)方法不是测试方法。 就像在Java中如何将方法视为虚拟的(多态的),除非您最终声明该方法。 这样一来,您传达的意图就会越来越少,不是吗?
我要说的是,使用有意义的约定没有什么错,它可以提高代码的信噪比,减少混乱和减少打字。
注释或不注释
何时使用注释是一个有趣的问题。 答案“总是”和“从不”都不正确。 在某些地方,注释是合适的甚至是优雅的。 然后在某些地方可能不是最佳选择。
在以下情况下使用注释:
- 元数据是内在的
- 如果您使用的是关键字(如果该语言可用),则该关键字可能是注释的候选对象(例如,瞬态)。
- 与其他方式相比,它更易于表达和使用注释
- 例如,在Web服务中将方法标记为Web方法比编写xml配置文件说起来容易得多。
- 基于类,不特定于对象
- 表示在类级别的元数据,与该类的任何特定对象无关
如果出现以下情况,请不要使用注释:
- 仅仅因为它当前在xml配置中并不意味着它现在应该成为注释
- 不要盲目地将您的xml配置信息转换为注释<
- 您已经有一种优雅的方式来指定此
- 如果您表示这些信息的方式已经足够优雅并且足够了,请不要陷入使用批注的压力中。
- 元数据是您将要更改的东西
- 您希望对此进行修改或配置,例如,bean方法的安全设置-您可能希望随时对其进行修改。 这些可能最好留在配置文件中。
- 您的应用程序可以从约定而不是配置中找出细节
- 您只能配置(使用注释或其他方式)与显而易见的或默认的不同的内容。
结论
注释是对该语言的有趣补充。 它们为使用新的元数据扩展Java语言提供了强大的结构。 但是,我们需要花时间根据具体情况评估其优点。 “正确使用注释”是设计方面的考虑,应在应用程序开发中予以适当考虑-盲目接受或拒绝它都不是一个好主意。
注释嵌套注释