级别: 初级 Brett McLaughlin, 作者/编者, O'Reilly Media, Inc
2004 年 9 月 01 日 注
释,J2SE 5.0 (Tiger) 中的新功能,将非常需要的元数据工具引入核心 Java 语言。该系列文章分为两部分,在这第 1
部分中,作者 Brett McLaughlin 解释了元数据如此有用的原因,向您介绍了 Java 语言中的注释,并研究了 Tiger
的内置注释。
编程的一个最新的趋势,尤其是在 Java 编程方面,是使用
元数据。简单地说,元数据就是
关于数据的数据。元数据可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。许多元数据工具,如 XDoclet(请参阅
参考资料),将这些功能添加到核心 Java 语言中,暂时成为 Java 编程功能的一部分。
直到可以使用 J2SE 5.0(也叫做 Tiger,现在是第二个 beta 版本),核心 Java 语言才最接近具有 Javadoc 方法的元数据工具。您使用特殊的标签集合来标记代码,并执行
javadoc
命令来将这些标签转化成格式化的 HTML 页面,该页面说明标签所附加到的类。然而,Javadoc
是有缺陷的元数据工具,因为除了生成文档之外,您没有固定、实用、标准化的方式来将数据用于其他用途。HTML 代码经常混入到 Javadoc
输出中这一事实甚至更进一步降低了其用于任何其他目的的价值。
Tiger 通过名为
注释的新功能将一个更通用的元数据工
具合并到核心 Java 语言中。注释是可以添加到代码中的修饰符,可以用于包声明、类型声明、构造函数、方法、字段、参数和变量。Tiger
包含内置注释,还支持您自己编写的定制注释。本文将概述元数据的优点并向您介绍 Tiger 的内置注释。本系列文章的 第 2 部分将研究定制注释。我要感谢 O'Reilly Media, Inc.,他们非常慷慨地 允许我在本文中使用我关于 Tiger 的书籍的“注释”一章中的代码示例(请参阅
参考资料)。
元数据的价值
一
般来说,元数据的好处分为三类:文档编制、编译器检查和代码分析。代码级文档最常被引用。元数据提供了一种有用的方法来指明方法是否取决于其他方法,它们
是否完整,特定类是否必须引用其他类,等等。这确实非常有用,但对于将元数据添加到 Java 语言中来说,文档编制可能是 最不相关的理由。Javadoc 已经提供了非常容易理解和健壮的方法来文档化代码。另外,当已经存在文档编制工具,并且在大多数时候都工作得很好时,谁还要编写文档编制工具?
编译器检查
元数据更重要的优点是编译器可以使用它来执行基本的编译时检查。例如,您将在本文后面的
Override 注释中
看到 Tiger 引入了一个这样的注释,用于允许您指定一种方法覆盖超类中的另一种方法。Java
编译器可以确保在元数据中指明的行为实际发生在代码级别。如果从来没有找出过这种类型的 bug,这样做似乎有点傻,但是大多数年龄很大的 Java
编程老手都曾经花费至少多个晚上来查明他们的代码为什么不能用。当最后认识到方法的参数有错,且该方法实际上 没有 覆盖超类中的方法时,您可能更感到难受。使用元数据的工具有助于轻松地查明这种类型的错误,从而可以节省那些晚上来看长期进行的 Halo 联赛。
|
JSR 175
JSR 175,
Java 编程语言的元数据工具,为将元数据合并到核心 Java 语言中提供了正式理由和说明(请参阅
参考资料)。根据 JSR,注释“不直接影响程序的语义。然而,开发和部署工具可以读取这些注释,并以某种形式处理这些注释,可能生成其他 Java 编程语言源文件、XML 文档或要与包含注释的程序一起使用的其他构件。”
|
|
代码分析
可
以证明,任何好的注释或元数据工具的最好功能就是可以使用额外数据来分析代码。在一个简单的案例中,您可能构建代码目录,提供必需的输入类型并指明返回类
型。但是,您可能想,Java
反射具有相同的优点;毕竟,可以为所有这些信息内省代码。这从表面上看似乎是正确的,但是在实际中通常不使用。许多时候,方法作为输入接受的或者作为输出
返回的类型实际上不是该方法想要的类型。例如,参数类型可能是 Object ,但方法可能仅使用
Integer 。这在好些情况下很容易发生,比如在方法被覆盖而超类使用常规参数声明方法时,还有正在进行许多序列化的系统中也容易发生。在这两种情况中,元数据可以指示代码分析工具,虽然参数类型是
Object ,但
Integer 才是真正需要的。这类分析非常有用,但也不能夸大它的价值。
在
更复杂的情况下,代码分析工具可以执行所有种类的额外任务。示例 du jour 是 Enterprise JavaBean (EJB)
组件。甚至简单 EJB 系统中的依赖性和复杂性都非常令人吃惊。您具有了 home 接口和远程接口,以及本地接口和本地 home
接口,还有一个实现类。保持所有这些类同步非常困难。但是,元数据可以提供这个问题的解决放案。好的工具(还是要提一下
XDoclet)可以管理所有这些依赖性,并确保无“代码级”连接、但有“逻辑级”捆绑的类保持同步。元数据在这里确实可以发挥它的作用。
注释的基本知识
现在已经了解了元数据的好处,我将介绍 Tiger 中的注释。注释采用“at”标记形式 (
@ ),后面是注释名称。然后在需要数据时,通过
name=value 对向注释提供数据。每次使用这类表示法时,就是在生成注释。一段代码可能会有 10 个、50 个或更多的注释。不过,您将发现多个注释都可能使用相同的
注释类型。类型是实际使用的结构,在特定上下文中,注释本身是该类型的具体使用(请参阅侧栏
注释或注释类型?)。
|
注释或注释类型?
是否对什么是注释与什么是注释类型感到迷惑?了解这个的最简单方法就是对比所熟悉的 Java 语言概念来想。可以定义一个类(例如
Person ),则在 JVM 中将总是仅有该类的一个版本(假设没有进行麻烦的类路径设置)。然而,在任何给定时间,可能会使用该类的 10 个或 20 个
实例。仍然是只有一个
Person 类,但是它以不同的方式使用多次。注释类型和注释也是这样。注释类型类似于类,注释类似于该类的实例。
|
|
注释分为三个基本种类:
-
标记注释没有变量。注释显示简单,由名称标识,没有提供其他数据。例如,
@MarkerAnnotation 是标记注释。它不包含数据,仅有注释名称。
-
单一值注释与标记注释类似,但提供一段数据。因为仅提供很少的一点数据,所以可以使用快捷语法(假设注释类型接受此语法):
@SingleValueAnnotation("my data") 。除了
@ 标记外,这应该与普通的 Java 方法调用很像。
-
完整注释有多个数据成员。因此,必须使用更完整的语法(注释不再像普通的 Java 方法):
@FullAnnotation(var1="data value 1", var2="data value 2", var3="data value 3") 。
除了通过默认语法向注释提供值外,还可以在需要传送多个值时使用名称-值对。还可以通过花括号为注释变量提供值数组。清单 1 显示了注释中的值数组的示例。
清单 1. 在注释中使用按数组排列的值
@TODOItems({ // Curly braces indicate an array of values is being supplied @TODO( severity=TODO.CRITICAL, item="Add functionality to calculate the mean of the student's grades", assignedTo="Brett McLaughlin" ), @TODO( severity=TODO.IMPOTANT, item="Print usage message to screen if no command-line flags specified", assignedTo="Brett McLaughlin" ), @TODO( severity=TODO.LOW, item="Roll a new website page with this class's new features", assignedTo="Jason Hunter" ) })
|
清单 1 中的示例并没有乍一看那样复杂。
TODOItems 注释类型有一个具有值的变量。这里提供的值比较复杂,但
TODOItems 的使用实际与单一值注释类型相符,只是这里的单一值是数组而已。该数组包含三个
TODO 注释,其中每个注释都是多值的。逗号分隔每个注释内的值,以及单个数组内的值。非常容易,是吧?
但是我讲的可能超前了些。
TODOItems 和
TODO 是
定制注释,
是本系列文章第 2 部分中的主题。但是我想让您看到,即使复杂注释(清单 1 几乎是最复杂的注释)也不是非常令人害怕的。当提到 Java
语言的标准注释类型时,将很少看到如此复杂的情况。正如将在下面三个部分了解到的,Tiger 的基本注释类型的使用都极其简单。
Override 注释
Tiger 的第一个内置注释类型是
Override 。
Override 应该仅用于方法(不用于类、包声明或其他构造)。它指明注释的方法将覆盖超类中的方法。清单 2 显示了简单的示例。
清单 2. 操作中的 Override 注释
package com.oreilly.tiger.ch06;
public class OverrideTester {
public OverrideTester() { }
@Override public String toString() { return super.toString() + " [Override Tester Implementation]"; }
@Override public int hashCode() { return toString().hashCode(); } }
|
清单 2 应该很容易理解。
@Override 注释对两个方法进行了注释 —
toString() 和
hashCode() ,来指明它们覆盖
OverrideTester 类的超类 (
java.lang.Object ) 中的方法的版本。开始这可能看起来没什么作用,但它实际上是非常好的功能。如果不覆盖这些方法,根本
无法 编译此类。该注释还确保当您将
toString() 弄乱时,至少还有某种指示,即应该确保
hashCode() 仍旧匹配。
当编码到很晚且输错了某些东西时,此注释类型真的可以发挥很大的作用,如清单 3 中所示。
清单 3. 使 Override 注释捕获打字稿
package com.oreilly.tiger.ch06;
public class OverrideTester {
public OverrideTester() { }
@Override public String toString() { return super.toString() + " [Override Tester Implementation]"; }
@Override public int hasCode() { return toString().hashCode(); } }
|
在清单 3 中,
hashCode() 错误地输入为
hasCode() 。注释指明
hasCode() 应该覆盖方法。但是在编译中,
javac 将发现超类 (
java.lang.Object ) 没有名为
hasCode() 的方法可以覆盖。因此,编译器将报错,如图 1 中所示。
图 1. 来自 Override 注释的编译器警告
|
缺少的功能
在单一值注释类型中,如果
Deprecated 允许包含错误类型消息将更好。然后,当用户使用声明为过时的方法时,编译器可以打印消息。该消息可以指明使用方法的结果如何重要,说明何时将停止方法,甚至建议备用方法。可能 J2SE 的下一版本(“Mustang”,他们这样命名)将提供这种功能。
|
|
这个便捷的小功能将帮助快速捕获打字稿。
Deprecated 注释
下一个标准注释类型是
Deprecated 。与
Override 一样,
Deprecated 是标记注释。正如您可能期望的,可以使用
Deprecated 来对不应再使用的方法进行注释。与
Override 不一样的是,
Deprecated 应该与正在声明为过时的方法放在同一行中(为什么这样?说实话我也不知道),如清单 4 中所示。
清单 4. 使用 Deprecated 注释
package com.oreilly.tiger.ch06;
public class DeprecatedClass {
@Deprecated public void doSomething() { // some code }
public void doSomethingElse() { // This method presumably does what doSomething() does, but better } }
|
单独编译此类时,不会发生任何不同。但是如果通过覆盖或调用来使用声明为过时的方法,编译器将处理注释,发现不应该使用该方法,并发出错误消息,如图 2 中所示。
图 2. 来自 Deprecated 注释的编译器警告
注意需要开启编译器警告,就像是必须向 Java 编译器指明想要普通的声明为过时警告。可以使用下列两个标记之一和
javac 命令:
-deprecated 或新的
-Xlint:deprecated 标记。
SuppressWarnings 注释
从 Tiger “免费”获得的最后一个注释类型是
SuppressWarnings 。发现该类型的作用应该不难,但是
为什么该注释类型如此重要通常不是很明显。它实际上是 Tiger 的所有新功能的副功能。例如,以泛型为例;泛型使所有种类的新类型安全操作成为可能,特别是当涉及 Java 集合时。然而,因为泛型,当使用集合而
没有 类型安全时,编译器将抛出警告。这对于针对 Tiger 的代码有帮助,但它使得为 Java 1.4.x 或更早版本编写代码非常麻烦。将不断地收到关于根本无关的事情的警告。如何才能使编译器不给您增添麻烦?
SupressWarnings 可以解决这个问题。
SupressWarnings 与
Override 和
Deprecated 不同,
是具有变量的 — 所以您将单一注释类型与该变量一起使用。可以以值数组来提供变量,其中每个值指明要阻止的一种特定警告类型。请看清单 5 中的示例,这是 Tiger 中通常会产生错误的一些代码。
清单 5. 不是类型安全的 Tiger 代码
public void nonGenericsMethod() { List wordList = new ArrayList(); // no typing information on the List
wordList.add("foo"); // causes error on list addition }
|
图 3 显示了清单 5 中代码的编译结果。
图 3. 来自非标准代码的编译器警告
清单 6 通过使用
SuppressWarnings 注释消除了这种问题。
清单 6. 阻止警告
@SuppressWarings(value={"unchecked"}) public void nonGenericsMethod() { List wordList = new ArrayList(); // no typing information on the List
wordList.add("foo"); // causes error on list addition }
|
非常简单,是吧?仅需要找到警告类型(图 3 中显示为“unchecked”),并将其传送到
SuppressWarnings 中。
SuppressWarnings 中变量的值采用数组,使您可以在同一注释中阻止多个警告。例如,
@SuppressWarnings(value={"unchecked", "fallthrough"}) 使用两个值的数组。此功能为处理错误提供了非常灵活的方法,无需进行大量的工作。
结束语
虽
然这里看到的语法可能都是新的,但您应该知道注释非常容易理解和使用。也就是说,与 Tiger
一起提供的标准注释相当简单,可以添加许多功能。元数据正日益变得有帮助,您肯定会提出非常适用于自己的应用程序的注释类型。在本系列文章的第 2
部分,我将详细说明 Tiger 对编写自己的注释类型的支持。您将了解如何创建 Java
类以及将其定义为注释类型,如何使编译器识别您的注释类型,以及如何使用该类型对代码进行注释。我甚至会更深入地说明奇异但有用的对注释进行注释的任务。
您将快速熟悉 Tiger 中的这一新构造。
参考资料
关于作者 | | Brett
McLaughlin 从 Logo 时代(还记得那个小三角吗?)就开始从事计算机工作。在最近几年里,他已经成为 Java 和 XML
社区最知名的作者和程序员之一。他曾经在 Nextel Communications 实现复杂的企业系统,在 Lutris
Technologies 编写应用程序服务器,目前在为 O'Reilly Media, Inc 撰写和编辑 书籍。
|
|