Java17 教程·续(一)
原文:More Java 17
一、注解
在本章中,您将学习:
-
什么是注解
-
如何声明注解
-
如何使用注解
-
什么是元注解以及如何使用它们
-
常用的批注,用于弃用 API、取消命名的编译时警告、重写方法和声明函数接口
-
如何在运行时访问注解
-
如何处理源代码中的注解
本章中的所有示例程序都是清单 1-1 中声明的jdojo.annotation模块的成员。
// module-info.java
module jdojo.annotation {
exports com.jdojo.annotation;
}
Listing 1-1The Declaration of a jdojo.annotation Module
什么是注解?
在我定义注解并讨论它们在编程中的重要性之前,让我们看一个简单的例子。假设您有一个Employee类,它有一个名为setSalary()的方法来设置雇员的工资。该方法接受一个类型为double的参数。下面的代码片段显示了Employee类的一个简单实现:
public class Employee {
public void setSalary(double salary) {
System.out.println("Employee.setSalary():" +
salary);
}
}
一个Manager类继承自Employee类。您希望为经理设置不同的工资。您决定覆盖Manager类中的setSalary()方法。Manager类的代码如下:
public class Manager extends Employee {
// Override setSalary() in the Employee class
public void setSalary(int salary) {
System.out.println("Manager.setSalary():" +
salary);
}
}
当您试图覆盖setSalary()方法时,Manager类中有一个错误。你很快就会改正错误。您使用了int数据类型作为被错误覆盖的方法的参数类型。是时候给经理定工资了。下面的代码用于实现这一点:
Employee ken = new Manager();
int salary = 200;
ken.setSalary(salary);
Employee.setSalary():200.0
这段代码应该调用Manager类的setSalary()方法,但是输出没有显示预期的结果。
您的代码中哪里出错了?在Manager类中定义setSalary()方法的目的是覆盖Employee类的setSalary()方法,而不是重载它。你犯了个错误。您使用类型int作为setSalary()方法中的参数类型,而不是Manager类中的类型double。您可以在Manager类中添加注解,表明您打算覆盖该方法。但是,评论并不能阻止你犯逻辑错误。您可能会像每个程序员一样,花费数小时来调试由这种逻辑错误导致的错误。在这种情况下谁能帮助你?在类似这样的情况下,注解可能会对您有所帮助。
让我们使用注解重写您的Manager类。此时,您不需要了解任何关于注解的知识。你要做的就是在你的程序中添加一个单词。以下代码是Manager类的修改版本:
public class Manager extends Employee {
@Override
public void setSalary(int salary) {
System.out.println("Manager.setSalary():" +
salary);
}
}
您所添加的只是对Manager类的一个@Override注解,并删除了“愚蠢的”注解。试图编译修改后的Manager类会导致编译时错误,该错误指向对Manager类的setSalary()方法使用了@Override注解:
Manager.java:2: error: method does not override or
implement a method from a supertype
@Override
^
1 error
使用@Override注解达到了目的。@Override注解与非静态方法一起使用,表示程序员打算在超类中覆盖该方法。在源代码级别,它服务于文档的目的。当编译器遇到@Override注解时,它会确保该方法确实覆盖了超类中的方法。如果注解的方法没有覆盖超类中的方法,编译器将生成一个错误。在您的例子中,Manager类中的setSalary(int salary)方法没有覆盖超类Employee中的任何方法。这就是你出错的原因。您可能会意识到使用注解就像记录源代码一样简单。但是,它们有编译器支持。您可以使用它们来指示编译器实现一些规则。注解提供的好处比您在这个例子中看到的要多得多。让我们回到编译时错误。您可以通过执行以下两项操作之一来修复错误:
-
您可以从
Manager类的setSalary(int salary)方法中移除@Override注解。它将使该方法成为重载方法,而不是重写其超类方法的方法。 -
您可以将方法签名从
setSalary(int salary)更改为setSalary(double salary)。
因为您想覆盖Manager类中的setSalary()方法,所以使用第二个选项并修改Manager类,如下所示:
public class Manager extends Employee {
@Override
public void setSalary(double salary) {
System.out.println("Manager.setSalary():" +
salary);
}
}
现在,以下代码将按预期工作:
Employee ken = new Manager();
int salary = 200;
ken.setSalary(salary);
Manager.setSalary():200.0
注意,Manager类的setSalary()方法中的@Override注解可以节省调试时间。假设您更改了Employee类中的方法签名。如果在Employee类中的改变使得这个方法不再在Manager类中被覆盖,那么当你再次编译Manager类时,你将得到同样的错误。你开始理解注解的力量了吗?有了这个背景,让我们开始深入研究注解。
根据韦氏词典词典,注解的意思是
通过评论或解释的方式添加的注解。
这正是 Java 中注解的含义。它允许您将元数据(或注解)关联到 Java 程序中的程序元素。程序元素可以是模块、包、类、接口、类的字段、局部变量、方法、方法的参数、枚举、注解、通用类型/方法声明中的类型参数、类型使用等。换句话说,您可以注解 Java 程序中的任何声明或类型使用。注解在程序元素的声明中作为“修饰符”使用,就像任何其他修饰符(public、private、final、static 等)一样。).与修饰符不同,注解不会修改程序元素的含义。它就像它所注解的程序元素的装饰或注解。
注解在许多方面不同于常规文档。一个常规的文档只供人类阅读,它是“愚蠢的”它没有相关的智能。如果您拼错了一个单词,或者在文档中陈述了一些东西,而在代码中做了相反的事情,那么您只能靠自己了。在运行时以编程方式读取文档的元素是非常困难和不切实际的。Java 允许你从你的文档中生成 Javadocs,这就是常规文档。这并不意味着你不需要记录你的程序。你确实需要正规的文件。同时,您需要一种使用类似文档的机制来执行您的意图的方法。您的文档应该对编译器和运行时可用。一个注解服务于这个目的。它是人类可读的,可以作为文档。它是编译器可读的,让编译器验证程序员的意图;例如,如果遇到方法的@Override注解,编译器会确保程序员确实覆盖了该方法。注解在运行时也是可用的,因此程序可以出于任何目的读取和使用它。例如,一个工具可以读取注解并生成样板代码。如果您使用过 Enterprise JavaBeans (EJB ),您就会知道保持所有接口和类同步以及向 XML 配置文件添加条目的痛苦。EJB 3.0 使用注解来生成样板代码,这使得 EJB 开发对程序员来说没有痛苦。框架/工具中使用注解的另一个例子是 JUnit 版。JUnit 是 Java 程序的单元测试框架。它使用注解来标记作为测试用例的方法。在此之前,您必须遵循测试用例方法的命名约定。注解有多种用途,包括文档、验证、编译器的执行、运行时验证、框架/工具的代码生成等。
要使注解对编译器和运行时可用,注解必须遵循规则。事实上,注解是类和接口的另一种类型。由于您必须在使用类类型或接口类型之前声明它们,因此您还必须声明注解类型。
注解不会改变它所注解的程序元素的语义(或含义)。从这个意义上说,注解就像注解一样,不会影响被注解的程序元素的工作方式。例如,setSalary()方法的@Override注解并没有改变方法的工作方式。你(或者一个工具/框架)可以基于一个注解改变一个程序的行为。在这种情况下,您使用注解,而不是注解自己做任何事情。关键是注解本身总是被动的。
声明注解类型
除了一些限制之外,声明注解类型类似于声明接口类型。根据 Java 规范,注解类型声明是一种特殊的接口类型声明。您可以使用 interface 关键字来声明注解类型,该关键字前面有@符号(at 符号)。以下是声明注解类型的一般语法:
[modifiers] @ interface <annotation-type-name> {
// Annotation type body goes here
}
注解声明的[modifiers]与interface声明相同。例如,您可以在公共或包级别声明注解类型。@符号和interface关键字可以用空格分开,也可以放在一起。按照惯例,他们被放在一起作为@interface。interface关键字后面是注解类型名。它应该是有效的 Java 标识符。注解类型 body 放在大括号内。
假设您想要用版本信息来注解您的程序元素,那么您可以准备一份关于在您产品的特定版本中添加的新程序元素的报告。要使用定制注解类型(与内置注解相反,如@Override),您必须首先声明它。您希望在版本信息中包含发行版本的主要版本和次要版本。清单 1-2 包含了第一个注解声明的完整代码。
// Version.java
package com.jdojo.annotation;
public @interface Version {
int major();
int minor();
}
Listing 1-2The Declaration of an Annotation Type Named Version
比较Version注解的声明和接口的声明。它与接口定义的区别仅在于一个方面:它在名称前使用了@符号。您在Version注解类型中声明了两个抽象方法:major()和minor()。注解类型中的抽象方法被称为其元素。你可以换一种方式思考:一个注解可以声明零个或多个元素,它们被声明为抽象方法。抽象方法名是注解类型的元素名。您已经为Version注解类型声明了两个元素major和minor。两个元素的数据类型都是 int。
Note
虽然您可以在接口类型中声明静态和默认方法,但是它们不允许在批注类型中使用。静态和默认方法意味着包含一些逻辑。注解意味着只表示注解类型中元素的值。这就是注解类型中不允许使用静态和默认方法的原因。
您需要编译注解类型。当Version.java文件被编译时,会产生一个Version.class文件。您的注解类型的简单名称是Version,它的完全限定名称是com.jdojo.annotation.Version。使用注解类型的简单名称遵循任何其他类型(例如,类、接口等)的规则。).您需要像导入任何其他类型一样导入注解类型。
如何使用注解类型?您可能认为您将声明一个实现Version注解类型的新类,并且您将创建该类的一个对象。您可能会松一口气,因为您不需要采取任何额外的步骤来使用Version注解类型。注解类型一旦被声明和编译,就可以使用了。要创建注解类型的实例并使用它来注解程序元素,需要使用以下语法:
@annotationType(name1=value1, name2=value2, name3=value3...)
注解类型前面有一个@符号。接下来是一列用圆括号括起来的逗号分隔的name=value对。name=value对中的名称是注解类型中声明的元素的名称,值是用户为该元素提供的值。name=value对不必按照注解类型中声明的顺序出现,尽管按照惯例name=value对的使用顺序与注解类型中元素声明的顺序相同。
让我们使用一个Version类型的实例,它的主要元素值为 1,次要元素值为 0。下面是您的Version注解类型的一个实例:
@Version(major=1, minor=0)
您可以将该注解重写为@Version(minor=0, major=1)而不改变其含义。您也可以使用批注类型的完全限定名作为
@com.jdojo.annotation.Version(major=0, minor=1)
您可以在程序中使用任意多的Version注解类型的实例。例如,您有一个VersionTest类,它从 1.0 版本开始就存在于您的应用程序中。您已经在 1.1 版中添加了一些方法和实例变量。您可以使用您的Version注解来记录不同版本中对VersionTest类的添加。您可以将类声明注解为
@Version(major=1, minor=0)
public class VersionTest {
// Code goes here
}
添加注解的方式与为程序元素添加修饰符的方式相同。您可以将程序元素的注解与其其他修饰符混合使用。您可以将注解放在与其他修改器相同的行中,也可以放在单独的行中。是使用单独的线来放置注解,还是将它们与其他修饰符混合在一起,这是个人的选择。按照惯例,程序元素的注解放在所有其他修饰符之前。让我们遵循这个约定,将注解单独放在一行中,如图所示。以下两个声明在技术上是相同的:
// Style #1
@Version(major=1, minor=0) public class VersionTest {
// Code goes here
}
// Style #2
public @Version(major=1, minor=0)
class VersionTest {
// Code goes here
}
清单 1-3 显示了VersionTest类的样本代码。
// VersionTest.java
package com.jdojo.annotation;
// Annotation for class VersionTest
@Version(major=1, minor=0)
public class VersionTest {
// Annotation for instance variable xyz
@Version(major=1, minor=1)
private int xyz = 110;
// Annotation for constructor VersionTest()
@Version(major=1, minor=0)
public VersionTest() {
}
// Annotation for constructor VersionTest(int xyz)
@Version(major=1, minor=1)
public VersionTest(int xyz) {
this.xyz = xyz;
}
// Annotation for the printData() method
@Version(major=1, minor=0)
public void printData() {
}
// Annotation for the setXyz() method
@Version(major=1, minor=1)
public void setXyz(int xyz) {
// Annotation for local variable newValue
@Version(major=1, minor=2)
int newValue = xyz;
this.xyz = xyz;
}
}
Listing 1-3A VersionTest Class with Annotated Elements
在清单 1-3 中,您使用@Version注解来注解类声明、类字段、局部变量、构造函数和方法。在VersionTest类的代码中没有什么特别的。您只是向该类的各种元素添加了@Version注解。即使您删除了所有的@Version注解,这个VersionTest类也会同样工作。需要强调的是,在程序中使用注解根本不会改变程序的行为。注解的真正好处来自于在编译时和运行时读取它。
接下来你会对Version注解类型做什么?您已经将其声明为类型。你在你的VersionTest课上用过。下一步是在运行时读取它。让我们暂时推迟这一步;我将在后面的章节中详细介绍它。我首先讨论更多关于注解类型声明的内容。
注解类型的限制
注解类型是一种特殊类型的接口,有一些限制。我将在接下来的章节中介绍一些限制。
限制#1
批注类型不能从另一个批注类型继承。也就是说,不能在批注类型声明中使用 extends 子句。以下声明将不会编译,因为您使用了 extends 子句来声明WrongVersion注解类型:
// Won't compile
public @interface WrongVersion extends BasicVersion {
int extended();
}
每个注解类型都隐式继承自java.lang.annotation.Annotation接口,声明如下:
package java.lang.annotation;
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
这意味着在Annotation接口中声明的所有四个方法在所有注解类型中都可用。
Caution
使用抽象方法声明来声明注解类型的元素。在Annotation接口中声明的方法不声明注解类型中的元素。您的Version注解类型只有两个元素,major和minor,它们是在Version类型本身中声明的。您不能将注解类型Version用作@Version(major=1, minor=2, toString="Hello")。Version注解类型没有将toString声明为元素。它从Annotation接口继承了toString()方法。
Annotation接口中的前三个方法是来自Object类的方法。annotationType()方法返回注解实例所属的注解类型的类引用。Java 在运行时动态创建一个代理类,它实现了注解类型。当您获得一个注解类型的实例时,该实例类就是动态生成的代理类,您可以使用注解实例上的getClass()方法获得它的引用。如果您在运行时获得了一个Version注解类型的实例,它的getClass()方法将返回动态生成的代理类的类引用,而它的annotationType()方法将返回com.jdojo.annotation.Version注解类型的类引用。
限制#2
批注类型中的方法声明不能指定任何参数。一个方法为注解类型声明一个元素。批注类型中的元素允许您将数据值与批注的实例相关联。注解中的方法声明不会被调用来执行任何类型的处理。把一个元素想象成一个类中的实例变量,这个类有两个方法,一个 setter 和一个 getter。对于注解,Java 运行时创建一个实现注解类型的代理类(这是一个接口)。每个注解实例都是代理类的一个对象。您在注解类型中声明的方法成为您在注解中指定的元素的值的 getter 方法。例如,参见清单 1-2 中的int major();和int minor();方法声明。Java 运行时将负责为注解元素设置指定的值。因为在注解类型中声明方法的目的是使用数据元素,所以不需要(也不允许)在方法声明中指定任何参数。下面的批注类型声明无法编译,因为它声明了一个 concatenate()方法,该方法接受两个参数:
// Won't compile
public @interface WrongVersion {
// Cannot have parameters
String concatenate(int major, int minor);
}
限制#3
批注类型中的方法声明不能有throws子句。注解类型中的方法被定义为表示数据元素。抛出异常来表示数据值是没有意义的。由于major()方法有一个throws子句,下面的注解类型声明无法编译:
// Won't compile
public @interface WrongVersion {
int major() throws Exception; // Cannot have a
// throws clause
int minor(); // OK
}
限制#4
在批注类型中声明的方法的返回类型必须是以下类型之一:
-
任意原始类型:
byte、short、int、long、float、double、boolean和char -
java.lang.String -
java.lang.Class -
枚举类型
-
注解类型
-
前面提到的任何类型的数组,例如,
String[]、int[]等。返回类型不能是嵌套数组。例如,您不能拥有返回类型String[][]或int[][]。
Note
这些数据类型限制背后的原因是,允许的数据类型的所有值都必须在源代码中表示,编译器应该能够表示这些值以便进行编译时分析。
Class的返回类型需要稍微解释一下。代替Class类型,您可以使用一个通用的返回类型,它将返回一个用户定义的类类型。假设你有一个Test类,你想在Test类型的注解类型中声明一个方法的返回类型。您可以声明注解方法,如下所示:
public @interface GoodOne {
Class element1();
// <- Any Class type
Class<Test> element2();
// <- Only Test class type
Class<? extends Test> element3();
// <- Test or its subclass type
}
限制#5
注解类型不能声明方法,这相当于覆盖了Object类或Annotation接口中的方法。
限制#6
批注类型不能是泛型。
注解元素的默认值
注解类型声明的语法允许您为其元素指定默认值。对于在其声明中指定了缺省值的注解元素,不要求也可以指定值。可以使用以下通用语法指定元素的默认值:
[modifiers] @interface <annotation-type-name> {
<data-type> <element-name>() default <default-value>;
}
关键字default用于指定默认值。类型的默认值必须与元素的数据类型兼容。
假设您有一个不经常发布的产品,那么它不太可能有一个非零的次要版本。您可以通过将次要元素的默认值指定为零来简化您的Version注解类型,如下所示:
public @interface Version {
int major();
int minor() default 0; // Set zero as default value
// for minor
}
一旦为元素设置了默认值,在使用这种类型的注解时就不必传递它的值。Java 将使用缺省元素的缺省值:
@Version(major=1) // minor is zero, which is
// its default value
@Version(major=2) // minor is zero, which is
// its default value
@Version(major=2, minor=1) // minor is 1, which is the
// specified value
所有默认值都必须是编译时常量。如何指定数组类型的默认值?你需要使用数组初始化语法。以下代码片段显示了如何为数组和其他数据类型指定默认值:
// Shows how to assign default values to elements of
// different types
public @interface DefaultTest {
double d() default 12.89;
int num() default 12;
int[] x() default {1, 2};
String s() default "Hello";
String[] s2() default {"abc", "xyz"};
Class c() default Exception.class;
Class[] c2() default {Exception.class,
java.io.IOException.class};
}
元素的默认值不与注解一起编译。当程序试图在运行时读取元素的值时,从注解类型定义中读取它。例如,当您使用@Version(major=2)时,这个注解实例会按原样编译。它不添加默认值为零的minor元素。换句话说,这个注解在编译时没有被修改为@Version(major=2, minor=0)。然而,当您在运行时读取这个注解的minor元素的值时,Java 将检测到没有指定minor元素的值。它将参考Version注解类型定义以获得其默认值。这种机制的含义是,如果您更改了一个元素的默认值,无论何时程序试图读取它,都会读取更改后的默认值,即使带注解的程序是在您更改默认值之前编译的。
注解类型及其实例
我经常使用术语“注解类型”和“注解”。注解类型是一种类似于接口的类型。理论上,只要可以使用接口类型,就可以使用注解类型。实际上,我们将它的使用仅限于注解程序元素。您可以声明注解类型的变量,如下所示:
Version v = null; // Here, Version is an annotation type
像接口一样,您也可以在类中实现注解类型。但是,您永远不应该这样做,因为这将违背将注解类型作为新构造的目的。您应该总是在类中实现接口,而不是注解类型。从技术上讲,清单 1-4 中用于DoNotUseIt类的代码是有效的。这只是为了演示的目的。即使可以工作,也不要在类中实现注解。
// DoNotUseIt.java
package com.jdojo.annotation;
import java.lang.annotation.Annotation;
public class DoNotUseIt implements Version {
// Implemented method from the Version annotation
// type
@Override
public int major() {
return 0;
}
// Implemented method from the Version annotation
// type
@Override
public int minor() {
return 0;
}
// Implemented method from the Annotation annotation
// type, which is the supertype of the Version
// annotation type
@Override
public Class<? extends Annotation> annotationType() {
return null;
}
}
Listing 1-4A Class Implementing an Annotation Type
Java 运行时实现了代理类的注解类型。它为您提供了一个类的对象,为您在程序中使用的每个注解实现您的注解类型。您必须区分注解类型和该注解类型的实例(或对象)。在您的示例中,Version是一个注解类型。每当您将它用作@Version(major=2, minor=4)时,您就创建了一个Version注解类型的实例。注解类型的实例简称为注解。例如,我们说@Version(major=2, minor=4)是一个注解或者是Version注解类型的一个实例。注解应该易于在程序中使用。语法@Version(...)是创建一个类、创建该类的一个对象以及设置其元素值的简写。我将在本章的后面讲述如何在运行时获得一个注解类型的对象。
使用注解
在这一节中,我将讨论在声明注解类型时使用不同类型元素的细节。请记住,为批注元素提供的值必须是编译时常量表达式,并且不能将 null 用作批注中任何类型元素的值。
原始类型
注解类型中的元素的数据类型可以是任何原始数据类型:byte、short、int、long、float、double、boolean和char。Version注解类型声明了两个元素major和minor,并且都是int数据类型。下面的代码片段声明了一个名为PrimitiveAnnTest的注解类型:
public @interface PrimitiveAnnTest {
byte a();
short b();
int c();
long d();
float e();
double f();
boolean g();
char h();
}
您可以使用一个PrimitiveAnnTest类型的实例作为
@PrimitiveAnnTest(a=1, b=2, c=3, d=4, e=12.34F, f=1.89, g=true, h='Y')
您可以使用编译时常数表达式来指定注解元素的值。以下两个Version注解实例是有效的,它们的元素具有相同的值:
@Version(major=2+1, minor=(int)13.2)
@Version(major=3, minor=13)
字符串类型
您可以在注解类型中使用String类型的元素。清单 1-5 包含名为Name的注解类型的代码。它有两个元素,first和last,属于String类型。
// Name.java
package com.jdojo.annotation;
public @interface Name {
String first();
String last();
}
Listing 1-5Name Annotation Type, Which Has Two Elements, first and last, of the String Type
以下代码片段显示了如何在程序中使用Name注解类型:
@Name(first="John", last="Jacobs")
public class NameTest {
@Name(first="Wally", last="Inman")
public void aMethod() {
// More code goes here...
}
}
在String类型元素的值表达式中使用字符串串联运算符(+)是有效的。以下两个注解是等效的:
@Name(first="Jo" + "hn", last="Ja" + "cobs")
@Name(first="John", last="Jacobs")
通常,当您想要使用编译时常量(如final class变量)作为注解元素值的一部分时,您会在注解中使用字符串串联。在下面的注解中,Test是一个类,它定义了一个名为UNKNOWN的编译时常量String类变量:
@Name(first="Mr. " + Test.UNKNWON, last=Test.UNKNOWN)
因为表达式new String("John")不是编译时常量表达式,所以@Name注解的以下用法无效:
@Name(first=new String("John"), last="Jacobs")
类别类型
在注解类型中使用Class类型作为元素的好处并不明显。通常,它用于工具/框架读取带有类类型元素的注解,并对元素的值执行一些专门的处理或生成代码的情况。让我们看一个使用类类型元素的简单例子。假设您正在编写一个测试运行器工具,用于运行 Java 程序的测试用例。您的注解将用于编写测试用例。如果您的测试用例在被测试运行程序调用时必须抛出一个异常,那么您需要使用一个注解来指出这一点。让我们创建一个DefaultException类,如清单 1-6 所示。
// DefaultException.java
package com.jdojo.annotation;
public class DefaultException
extends java.lang.Throwable {
public DefaultException() {
}
public DefaultException(String msg) {
super(msg);
}
}
Listing 1-6A DefaultException Class That Is Inherited from the Throwable Exception Class
清单 1-7 显示了一个TestCase注解类型的代码。
// TestCase.java
package com.jdojo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestCase {
Class<? extends Throwable> willThrow() default
DefaultException.class;
}
Listing 1-7A TestCase Annotation Type Whose Instances Are Used to Annotate Test Case Methods
willThrow元素的返回类型被定义为Throwable类的通配符,因此用户将只指定Throwable类或其子类作为元素的值。您可以使用Class<?>类型作为 willThrow 元素的类型。然而,这将允许该注解类型的用户传递任何类类型作为它的值。注意,您已经为TestCase注解类型使用了两个注解,@Retention和@Target。@Retention注解类型指定@TestCase注解将在运行时可用。有必要对您的TestCase注解类型使用RUNTIME的保留策略,因为这意味着测试运行器工具将在运行时读取它。@Target注解声明TestCase注解只能用于注解方法。在后面讨论元注解时,我将详细介绍@Retention和@Target注解类型。清单 1-8 展示了TestCase注解类型的使用。
// PolicyTestCases.java
package com.jdojo.annotation;
import java.io.IOException;
public class PolicyTestCases {
// Must throw IOException
@TestCase(willThrow=IOException.class)
public static void testCase1(){
// Code goes here
}
// We are not expecting any exception
@TestCase()
public static void testCase2(){
// Code goes here
}
}
Listing 1-8A Test Case That Uses the TestCase Annotations
testCase1()方法使用@TestCase注解指定它将抛出一个IOException。测试运行工具将确保当它调用这个方法时,这个方法确实抛出了一个IOException。否则,它将无法通过测试用例。testCase2()方法没有指定它将抛出一个异常。如果它在测试运行时抛出了一个异常,那么这个工具应该会使这个测试用例失败。
枚举类型
批注可以包含枚举类型的元素。假设您想要声明一个名为Review的注解类型,它可以描述程序元素的代码审查状态。让我们假设它有一个状态元素,它可以有四个值之一:PENDING、FAILED、PASSED和PASSEDWITHCHANGES。您可以将枚举声明为批注类型成员。清单 1-9 显示了一个Review注解类型的代码。
// Review.java
package com.jdojo.annotation;
public @interface Review {
ReviewStatus status() default ReviewStatus.PENDING;
String comments() default "";
// ReviewStatus enum is a member of the Review
// annotation type
public enum ReviewStatus {PENDING, FAILED, PASSED,
PASSEDWITHCHANGES};
}
Listing 1-9An Annotation Type That Uses an enum Type Element
Note
用作批注元素类型的枚举类型不需要声明为批注类型的嵌套枚举类型,如本例所示。枚举类型也可以在批注类型之外声明。
Review注解类型声明了一个ReviewStatus枚举类型,四个审查状态是该枚举的元素。它有两个元素,status和comments。状态元素的类型是枚举类型ReviewStatus。状态元素的默认值为ReviewStatus.PENDING。您有一个空字符串作为 comments 元素的默认值。
下面是一些Review注解类型的实例。您需要在您的程序中导入com.jdojo.annotation.Review.ReviewStatus枚举,以使用ReviewStatus枚举类型的简单名称:
import com.jdojo.annotation.Review.ReviewStatus;
...
// Have default for status and comments. Maybe the code
// is new.
@Review()
// Leave status as Pending, but add some comments
@Review(comments=
"Have scheduled code review on December 1, 2017")
// Fail the review with comments
@Review(status=ReviewStatus.FAILED,
comments="Need to handle errors")
// Pass the review without comments
@Review(status=ReviewStatus.PASSED)
下面是注解测试类的示例代码,表明它通过了代码审查:
import com.jdojo.annotation.Review.ReviewStatus;
import com.jdojo.annotation.Review;
@Review(status=ReviewStatus.PASSED)
public class Test {
// Code goes here
}
注解类型
注解类型可以用在 Java 程序中任何可以使用类型的地方。例如,您可以使用注解类型作为方法的返回类型。您也可以使用注解类型作为另一个注解类型声明中的元素类型。假设您想要一个名为Description的新注解类型,它将包含作者姓名、版本和程序元素的注解。您可以重用您的Name和Version注解类型作为它的name和version元素类型。清单 1-10 显示了Description注解类型的代码。
// Description.java
package com.jdojo.annotation;
public @interface Description {
Name name();
Version version();
String comments() default "";
}
Listing 1-10An Annotation Type Using Other Annotation Types As Its Elements
要为注解类型的元素提供值,需要使用创建注解类型实例的语法。例如,@Version(major=1, minor=2)创建了一个Version注解的实例。请注意,在下面的代码片段中,一个批注嵌套在另一个批注内:
@Description(name=@Name(first="John", last="Jacobs"),
version=@Version(major=1, minor=2),
comments="Just a test class")
public class Test {
// Code goes here
}
数组类型注解元素
批注可以包含数组类型的元素。数组类型可以是以下类型之一:
-
原始类型
-
java.lang.String类型 -
java.lang.Class类型 -
枚举类型
-
注解类型
您需要为大括号内的数组元素指定值。数组的元素由逗号分隔。假设您想用一个简短的描述来注解您的程序元素,这个描述是您需要处理的事情的列表。清单 1-11 为此创建了一个ToDo注解类型。
// ToDo.java
package com.jdojo.annotation;
public @interface ToDo {
String[] items();
}
Listing 1-11ToDo Annotation Type with a String Array As Its Sole Element
以下代码片段显示了如何使用@ToDo注解:
@ToDo(items={"Add readFile method", "Add error handling"})
public class Test {
// Code goes here
}
如果数组中只有一个元素,可以省略大括号。
以下两个ToDo注解类型的注解实例是等效的:
@ToDo(items={"Add error handling"})
@ToDo(items="Add error handling")
Note
如果没有有效值传递给数组类型的元素,可以使用空数组。例如,@ToDo(items={})是一个有效的注解,其中 items 元素被分配了一个空数组。
批注中没有空值
不能使用null引用作为注解中元素的值。注意,允许对String类型元素使用空字符串,对数组类型元素使用空数组。使用以下注解将导致编译时错误:
@ToDo(items=null)
@Name(first=null, last="Jacobs")
速记注解语法
速记注解语法在某些情况下更容易使用。假设您有一个注解类型Enabled,它的元素有一个默认值,如下所示:
public @interface Enabled {
boolean status() default true;
}
如果您想用Enabled注解类型注解一个程序元素,并使用其元素的缺省值,那么您可以使用@Enabled()语法。您不需要为 status 元素指定值,因为它有一个默认值。在这种情况下,您可以使用速记,这样可以省略括号。你可以只用@Enabled而不用@Enabled()。Enabled注解可以使用以下两种形式:
@Enabled
public class Test {
// Code goes here
}
@Enabled()
public class Test {
// Code goes here
}
只有一个元素的注解类型也有简写语法。
如果遵循注解类型中唯一元素的命名规则,可以使用这种简写方式。元素的名称必须是value。如果一个注解类型只有一个名为value的元素,您可以在注解中省略name=value对中的名称。下面的代码片段声明了一个Company注解类型,它只有一个名为 value 的元素:
public @interface Company {
String value(); // the element name is value
}
使用Company注解时,可以省略name=value对中的名称,如下所示。如果你想使用带有Company注解的元素名,你总是可以这样做
@Company(value="Abc Inc.")
@Company("Abc Inc.")
public class Test {
// Code goes here
}
您可以使用这种从注解中省略元素名称的简写方式,即使元素数据类型是数组。考虑下面称为Reviewers的注解类型:
public @interface Reviewers {
String[] value(); // the element name is value
}
由于Reviewers注解类型只有一个元素,名为 value,所以在使用时可以省略元素名:
// No need to specify name of the element
@Reviewers({"John Jacobs", "Wally Inman"})
public class Test {
// Code goes here
}
如果在数组中只为Reviewers注解类型的 value 元素指定了一个元素,也可以省略大括号:
@Reviewers("John Jacobs")
public class Test {
// Code goes here
}
您刚刚看到了几个使用元素名称作为值的例子。这里是在注解中省略元素名称的一般规则:如果在使用注解时只提供一个值,则假定元素名称为value。这意味着您不需要在注解类型中只有一个名为value的元素,从而在注解中省略其名称。如果您有一个注解类型,它有一个名为value(有或没有缺省值)的元素,并且所有其他元素都有缺省值,您仍然可以在此类型的注解实例中省略该元素的名称。以下是一些说明这一规则的例子:
public @interface A {
String value();
int id() default 10;
}
// Same as @A(value="Hello", id=10)
@A("Hello")
public class Test {
// Code goes here
}
// Won't compile. Must use only one value to omit the
// element name
@A("Hello", id=16)
public class WontCompile {
// Code goes here
}
// OK. Must use name=value pair when passing more than
// one value
@A(value="Hello", id=16)
public class Test {
// Code goes here
}
标记注解类型
标记注解类型不声明任何元素,甚至不声明具有默认值的元素。通常,标记注解由注解处理工具使用,注解处理工具基于标记注解类型生成某种样板代码:
public @interface Marker {
// No element declarations
}
@Marker
public class Test {
// Code goes here
}
一个例子是由某个性能监控工具监控的方法的@Monitor注解:
public class Calculator {
...
@Monitor
public void calc() {
...
}
}
该工具将自动添加用于测量经过时间、呼叫频率等的代码。
元注解类型
元注解类型用于注解其他注解类型声明。以下是元注解类型:
-
Target -
Retention -
Inherited -
Documented -
Repeatable
元注解类型是 Java 类库的一部分。它们在java.lang.annotation包中声明。我将在后续章节中详细讨论元注解类型。
Note
java.lang.annotation包包含一个Native注解类型,它不是元注解。它用于注解字段,指示该字段可以从本机代码引用。这是一个标记注解。通常,它由基于该注解生成一些代码的工具使用。
目标注解类型
作为元注解集的第一个成员,Target注解类型用于指定注解类型可以使用的上下文。它只有一个名为 value 的元素,这是一个java.lang.annotation.ElementType枚举类型的数组。表 1-1 列出了ElementType枚举中的所有常量。
表 1-1
Java . lang . annotation . element type 枚举中的常数列表
|常数名称
|
描述
|
| --- | --- |
| ANNOTATION_TYPE | 用于注解另一个注解类型声明。这使得注解类型成为元注解。 |
| CONSTRUCTOR | 用于注解构造函数。 |
| FIELD | 用于注解字段和枚举常量。 |
| LOCAL_VARIABLE | 用于注解局部变量。 |
| METHOD | 用于注解方法。 |
| MODULE | 用于注解模块。它是在 Java 9 中添加的。 |
| PACKAGE | 用于注解包声明。 |
| PARAMETER | 用于标注参数。 |
| TYPE | 用于批注类、接口(包括批注类型)或枚举声明。 |
| TYPE_PARAMETER | 用于注解泛型类、接口、方法等中的类型参数。它是在 Java 8 中添加的。 |
| TYPE_USE | 用于注解所有类型的使用。它是在 Java 8 中添加的。在可以使用带有ElementType.TYPE和ElementType.TYPE_PARAMETER的注解的地方,也可以使用该注解。它也可以用在构造函数之前,在这种情况下,它表示由构造函数创建的对象。 |
下面的Version注解类型声明用Target元注解注解注解类型声明,元注解指定Version注解类型只能用于三种类型的程序元素:任意类型(类、接口、枚举和注解类型)、构造函数和方法。
// Version.java
package com.jdojo.annotation;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR,
ElementType.METHOD})
public @interface Version {
int major();
int minor();
}
除了在其Target注解中指定的三种类型外,Version注解类型不能用于任何程序元素。它的以下用法不正确,因为它正用于实例变量(字段):
public class WontCompile {
// A compile-time error. Version annotation cannot
// be used on a field.
@Version(major = 1, minor = 1)
int id = 110;
}
以下对Version注解的使用是有效的:
// OK. A class type declaration
@Version(major = 1, minor = 0)
public class VersionTest {
// OK. A constructor declaration
@Version(major = 1, minor = 0)
public VersionTest() {
// Code goes here
}
// OK. A method declaration
@Version(major = 1, minor = 1)
public void doSomething() {
// Code goes here
}
}
在 Java 8 之前,方法的形参以及包、类、方法、字段和局部变量的声明都允许使用注解。Java 8 增加了对在任何类型使用和类型参数声明中使用注解的支持。短语“任何类型的使用”需要一点解释。类型在许多上下文中使用,例如,在 extends 子句后作为超类型,在 new 运算符后的对象创建表达式中,在 cast 中,在 throws 子句中,等等。从 Java 8 开始,只要使用了类型,注解就可能出现在类型的简单名称之前。请注意,类型的简单名称只能用作名称,而不能用作类型,例如,在 import 语句中。考虑清单 1-12 和 1-13 中显示的Fatal和NonZero注解类型的声明。
// NonZero.java
package com.jdojo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.TYPE_USE})
public @interface NonZero {
}
Listing 1-13A NonZero Annotation Type That Can Be Used with Any Type Use
// Fatal.java
package com.jdojo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.TYPE_USE})
public @interface Fatal {
}
Listing 1-12A Fatal Annotation Type That Can Be Used with Any Type Use
在任何使用类型的地方都可以使用Fatal和NonZero注解类型。它们在以下上下文中的使用是有效的:
public class Test {
public void processData() throws @Fatal Exception {
double value = getValue();
int roundedValue = (@NonZero int) value;
Test t = new @Fatal Test();
// More code goes here
}
public double getValue() {
double value = 189.98;
// More code goes here
return value;
}
}
Note
如果没有用Target注解类型注解注解类型,注解类型可以在任何地方使用,除了在类型参数声明中。
保留注解类型
您可以将注解用于不同的目的。您可能希望仅出于文档目的使用它们,由编译器处理,和/或在运行时使用它们。注解可以在三个级别保留:
-
仅源代码
-
仅类文件(默认)
-
类文件和运行时
Retention元注解类型用于指定 Java 应该如何保留注解类型的注解实例。这也称为注解类型的保留策略。如果注解类型具有“仅源代码”保留策略,则在编译到类文件中时,该类型的实例将被删除。如果保留策略是“仅类文件”,注解实例将保留在类文件中,但在运行时无法读取。如果保留策略是“类文件和运行时”(简称为运行时),注解实例将保留在类文件中,并且可以在运行时读取。
Retention 元注解类型声明了一个名为 value 的元素,它属于java.lang.annotation.RetentionPolicy枚举类型。RetentionPolicy枚举有三个常量,SOURCE、CLASS和RUNTIME,分别用于指定仅源代码、仅类和类和运行时的保留策略。下面的代码在Version注解类型上使用了Retention元注解。它指定Version注解应该在运行时可用。注意在Version注解类型上使用了两个元注解:Target和Retention。
// Version.java
package com.jdojo.annotation;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR,
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Version {
int major();
int minor();
}
Note
如果不在注解类型上使用Retention元注解,其保留策略默认为仅类文件。这意味着您将无法在运行时阅读这些注解。一开始你会犯这种常见的错误。您会尝试读取注解,但运行时不会返回任何值。在运行时尝试读取它们之前,确保您的注解类型已经用保留策略为RetentionPolicy.RUNTIME的保留元注解进行了注解。无论注解类型的保留策略如何,局部变量声明上的注解在类文件中或运行时都是不可用的。这种限制的原因是 Java 运行时不允许您在运行时使用反射来访问局部变量;除非您有权在运行时访问局部变量,否则您无法读取它们的注解。
继承的批注类型
Inherited注解类型是标记元注解类型。如果注解类型是用Inherited元注解来注解的,那么它的实例会被子类声明继承。如果注解类型用于注解除类声明之外的任何程序元素,则没有任何效果。让我们考虑两个注解类型声明:Ann2和Ann3。注意,Ann2没有用Inherited元注解进行注解,而Ann3有。
public @interface Ann2 {
int id();
}
@Inherited
public @interface Ann3 {
int id();
}
让我们声明两个类,A和B,如下所示。注意,类B继承了类A:
@Ann2(id=505)
@Ann3(id=707)
public class A {
// Code for class A goes here
}
// Class B inherits Ann3(id=707) annotation from the
// class A
public class B extends A {
// Code for class B goes here
}
在这段代码中,类B从类A继承了@Ann3(id=707)注解,因为Ann3注解类型已经用Inherited元注解进行了注解。类B不继承@Ann2(id=505)注解,因为Ann2注解类型没有用Inherited元注解进行注解。
记录的注解类型
Documented注解类型是标记元注解类型。如果注解类型用Documented注解进行了注解,Javadoc 工具将为它的所有实例生成文档。清单 1-14 包含了Version注解类型的最终版本的代码,它已经用一个Documented元注解进行了注解。
// Version.java
package com.jdojo.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR,
ElementType.METHOD, ElementType.MODULE,
ElementType.PACKAGE, ElementType.LOCAL_VARIABLE,
ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
int major();
int minor();
}
Listing 1-14The Final Version of the Version Annotation Type
假设您用您的Version注解类型注解了一个Test类,如下所示:
package com.jdojo.annotation;
@Version(major=1, minor=0)
public class Test {
// Code for Test class goes here
}
当您使用 Javadoc 工具为Test类生成文档时,Test类声明上的Version注解也会作为文档的一部分生成。如果从Version注解类型声明中移除Documented注解,Test类文档将不会包含关于其Version注解的信息。
可重复注解类型
如果允许重复使用,注解类型声明必须用@Repeatable注解进行注解。Repeatable注解类型只有一个名为 value 的元素,其类型是另一个注解类型的类类型。创建可重复注解类型是一个两步过程:
-
声明一个注解类型(比如说
T,并用Repeatable元注解对其进行注解。将注解的值指定为另一个注解,该注解包含所声明的可重复注解类型的注解。 -
用一个元素声明包含的批注类型,该元素是可重复批注的数组。
清单 1-15 和 1-16 包含对ChangeLog和ChangeLogs注解类型的声明。ChangeLog被标注了@Repeatable(ChangeLogs.class)标注,这意味着它是一个可重复的标注类型,其包含的标注类型是ChangeLogs。
// ChangeLogs.java
package com.jdojo.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangeLogs {
ChangeLog[] value();
}
Listing 1-16A Containing Annotation Type for the ChangeLog Repeatable Annotation Type
// ChangeLog.java
package com.jdojo.annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ChangeLogs.class)
public @interface ChangeLog {
String date();
String comments();
}
Listing 1-15A Repeatable Annotation Type That Uses the ChangeLogs As the Containing Annotation Type
您可以使用ChangeLog注解来记录Test类的更改历史,如下所示:
@ChangeLog(date="08/28/2017",
comments="Declared the class")
@ChangeLog(date="09/21/2017",
comments="Added the process() method")
public class Test {
public static void process() {
// Code goes here
}
}
常用的标准注解
Java API 定义了许多标准的注解类型。本节讨论四种最常用的标准注解。它们在java.lang包中定义。他们是
-
Deprecated -
Override -
SuppressWarnings -
FunctionalInterface
贬低 API
在 Java 中弃用 API 是提供关于 API 生命周期信息的一种方式。您可以弃用模块、包、类型、构造函数、方法、字段、参数和局部变量。当你反对一个 API 的时候,你是在告诉它的用户
-
不要使用 API,因为它很危险
-
因为有更好的 API 替代品,所以从 API 中迁移出来
-
从 API 中迁移出来,因为该 API 将在未来的版本中被删除
JDK 包含两个用于弃用 API 的构造:
-
Javadoc 标签
-
java.lang.Deprecated标注类型
Javadoc 标签允许您使用 HTML 丰富的文本格式特性来指定关于弃用的细节。java.lang.Deprecated注解类型可以用在 API 元素上,但不推荐使用。
运行时会保留Deprecated注解类型。
标签@deprecated和注解@Deprecated应该一起使用。两者都应该出席或都缺席。@Deprecation注解不允许您指定弃用的描述,因此您必须使用@deprecated标签来提供描述。
Note
在 API 元素上使用@deprecated标签,而不是@Deprecated注解,会产生编译器警告。
清单 1-17 包含一个名为FileCopier的类的声明。假设这个类是作为库的一部分提供的。
// FileCopier.java
package com.jdojo.deprecation;
import java.io.File;
/**
* The class consists of static methods that can be used
* to copy files and directories.
*
* @deprecated Deprecated since 1.4\. Not safe to use. Use
* the <code>java.nio.file.Files</code> class instead. This
* class will be removed in a future release of this library.
*
* @since 1.2
*/
@Deprecated
public class FileCopier {
// No direct instantiation supported
private FileCopier() {
}
/**
* Copies the contents of src to dst.
* @param src The source file
* @param dst The destination file
* @return true if the copy is successfully,
* false otherwise.
*/
public static boolean copy(File src, File dst) {
// More code goes here
return true;
}
// More code goes here
}
Listing 1-17A FileCopier Utility Class
使用@Deprecated注解不赞成使用FileCopier类。它的 Javadoc 使用@deprecated标签给出弃用的详细信息,比如何时弃用、替换以及移除通知。在 JDK9 之前,@Deprecated注解类型不包含任何元素,所以您必须使用 Javadoc 中的@deprecated标签为不推荐的 API 提供关于不推荐的所有细节。请注意,Javadoc 中使用的@since标记表示FileCopier类从该库的 1.2 版本起就已经存在,而@deprecated标记表示该类从该库的 1.4 版本起已被废弃。
Javadoc 工具将@deprecated 标记的内容移到生成的 Javadoc 的顶部,以引起读者的注意。当不推荐使用的代码使用不推荐使用的 API 时,编译器会生成警告。用@Deprecated注解 API 不会生成警告;然而,使用一个用@Deprecated注解标注的 API 就可以了。如果您在类本身之外使用了FileCopier类,您将收到一个关于使用不推荐使用的类的编译时警告。
假设您编译了代码并将其部署到生产环境中。如果您升级了包含旧应用程序使用的新的、不推荐使用的 API 的 JDK 版本或库/框架,您不会收到任何警告,并且您将错过从不推荐使用的 API 中进行迁移的机会。您必须重新编译代码才能收到警告。没有工具可以扫描和分析编译后的代码(例如 JAR 文件)并报告废弃 API 的使用情况。更糟糕的情况是,当一个不推荐使用的 API 从新版本中删除时,您的旧的编译后的代码会收到意外的运行时错误。当开发人员看到不赞成使用的元素 Javadoc 时,他们也感到困惑——没有办法表达 API 何时不赞成使用,以及不赞成使用的 API 是否会在未来的版本中删除。在 JDK9 之前,您所能做的就是在文本中将这些信息指定为@deprecated 标记的一部分。出于这个原因,有两个额外的元素增强了@Deprecated注解(从 JDK9 开始):since和forRemoval。它们声明如下:
-
String since()默认为“”;
-
boolean forRemoval()默认值为 false
两个新元素都指定了默认值,因此注解的现有使用不会中断。since元素指定带注解的 API 元素被弃用的版本。它是一个字符串,您应该遵循与 JDK 版本方案相同的版本命名约定,例如,“9”代表 JDK9。它默认为空字符串。请注意,JDK9 没有向@Deprecated注解类型添加元素来指定弃用的描述。这样做有两个原因:
-
运行时会保留注解。向注解添加描述性文本会增加运行时内存。
-
描述性文本不能只是纯文本。例如,它需要提供一个链接来替换不推荐使用的 API。现有的@deprecated Javadoc 标记已经提供了此功能。
forRemoval元素表示带注解的 API 元素将在未来的版本中被移除,您应该从 API 中迁移出来。它默认为 false。
Note
元素上的@since Javadoc 标签指示 API 元素何时被添加,而@Deprecated注解的since元素指示 API 元素何时被弃用。在 JDK9 中,已经做出了合理的努力,在 Java SE APIs 中的@Deprecated注解的大多数(如果不是全部)使用位置中回填这两个元素的值。
在@Deprecation注解类型中添加了forRemoval元素之后,又增加了五个用例。当一个 API 被弃用并且forRemoval设置为 false 时,这样的弃用被称为普通弃用,在这种情况下发出的警告被称为普通弃用警告。当一个 API 被弃用并且forRemoval被设置为true时,这种弃用被称为终端弃用,在这种情况下发出的警告被称为终端弃用警告或移除警告。表 1-2 显示了弃用警告矩阵(在 JDK9 中发布)。
表 1-2
弃用警告矩阵
|API 使用-站点
|
API 声明站点,不推荐使用
|
API 声明站点,已过时
|
API 声明站点,通常不推荐使用
| | --- | --- | --- | --- | | 不推荐使用 | 没有警告 | 普通折旧警告 | 删除弃用警告 | | 通常已弃用 | 没有警告 | 没有警告 | 删除弃用警告 | | 已弃用 | 没有警告 | 没有警告 | 删除弃用警告 |
在一种情况下发出的警告需要稍微解释一下,在这种情况下,API 和它的使用站点最终都被否决了。API 和使用它的代码都已经被弃用了,而且它们都将在未来被移除,那么在这种情况下得到警告有什么意义呢?这样做是为了涵盖最终被否决的 API 和它的使用位置在两个不同的代码库中并且被独立维护的情况。如果 use-site 代码库比 API 代码库存在的时间长,use-site 将得到一个意外的运行时错误,因为它使用的 API 不再存在。在使用站点发布一个警告将会给它的维护者一个机会来计划替代方案,以防在使用站点的代码之前,最终被否决的 API 消失。
如果使用@SuppressWarnings("deprecation"),编译器只抑制普通的反对警告。要抑制删除警告,您需要使用@SuppressWarnings("removal")。如果你想抑制普通警告和删除警告,你需要使用@SuppressWarnings({"deprecation", "removal"})。
作为一个例子,我用一个简单的例子向您展示了不赞成使用的 API 的所有用例,使用了不赞成使用的 API,并且取消了警告。在示例中,我只反对方法,并使用它们来生成编译时警告。然而,你并不仅限于贬低方法。对这些方法的注解应该有助于您理解预期的行为。清单 1-18 包含一个名为Box的类的代码。该类包含三种方法,每种不推荐使用的类别中有一种,即不推荐使用、通常不推荐使用和最终不推荐使用。我保持了类的简单性,所以你可以把重点放在使用的弃用上。编译Box类不会生成任何不推荐使用的警告,因为该类不使用任何不推荐使用的 API 相反,它包含了不推荐使用的 API。
// Box.java
package com.jdojo.annotation;
/**
* This class is used to demonstrate how to deprecate APIs.
*/
public class Box {
/**
* Not deprecated
*/
public static void notDeprecated() {
System.out.println("notDeprecated...");
}
/**
* Deprecated ordinarily.
* @deprecated Do not use it.
*/
@Deprecated(since="2")
public static void deprecatedOrdinarily() {
System.out.println("deprecatedOrdinarily...");
}
/**
* Deprecated terminally.
* @deprecated It will be removed in a future release.
* Migrate your code now.
*/
@Deprecated(since="2", forRemoval=true)
public static void deprecatedTerminally() {
System.out.println("deprecatedTerminally...");
}
}
Listing 1-18A Box Class with Three Types of Methods: Not Deprecated, Ordinarily Deprecated, and Terminally Deprecated
清单 1-19 包含了一个BoxTest类的代码。该类使用了Box类的所有方法。在BoxTest类中的一些方法已经被弃用。前九个方法对应于表 1-2 中的九个用例,将生成四个弃用警告——一个普通警告和三个终止警告。像m4X()这样命名的方法,其中X是一个数字,向您展示了如何抑制普通的和最终的弃用警告。
// BoxTest.java
package com.jdojo.annotation;
public class BoxTest {
/**
* API: Not deprecated
* Use-site: Not deprecated
* Deprecation warning: No warning
*/
public static void m11() {
Box.notDeprecated();
}
/**
* API: Ordinarily deprecated
* Use-site: Not deprecated
* Deprecation warning: No warning
*/
public static void m12() {
Box.deprecatedOrdinarily();
}
/**
* API: Terminally deprecated
* Use-site: Not deprecated
* Deprecation warning: Removal warning
*/
public static void m13() {
Box.deprecatedTerminally();
}
/**
* API: Not deprecated
* Use-site: Ordinarily deprecated
* Deprecation warning: No warning
* @deprecated Dangerous to use.
*/
@Deprecated(since="1.1")
public static void m21() {
Box.notDeprecated();
}
/**
* API: Ordinarily deprecated
* Use-site: Ordinarily deprecated
* Deprecation warning: No warning
* @deprecated Dangerous to use.
*/
@Deprecated(since="1.1")
public static void m22() {
Box.deprecatedOrdinarily();
}
/**
* API: Terminally deprecated
* Use-site: Ordinarily deprecated
* Deprecation warning: Removal warning
* @deprecated Dangerous to use.
*/
@Deprecated(since="1.1")
public static void m23() {
Box.deprecatedTerminally();
}
/**
* API: Not deprecated
* Use-site: Terminally deprecated
* Deprecation warning: No warning
* @deprecated Going away.
*/
@Deprecated(since="1.1", forRemoval=true)
public static void m31() {
Box.notDeprecated();
}
/**
* API: Ordinarily deprecated
* Use-site: Terminally deprecated
* Deprecation warning: No warning
* @deprecated Going away.
*/
@Deprecated(since="1.1", forRemoval=true)
public static void m32() {
Box.deprecatedOrdinarily();
}
/**
* API: Terminally deprecated
* Use-site: Terminally deprecated
* Deprecation warning: Removal warning
* @deprecated Going away.
*/
@Deprecated(since="1.1", forRemoval=true)
public static void m33() {
Box.deprecatedTerminally();
}
/**
* API: Ordinarily and Terminally deprecated
* Use-site: Not deprecated
* Deprecation warning: Ordinary and removal warnings
*/
public static void m41() {
Box.deprecatedOrdinarily();
Box.deprecatedTerminally();
}
/**
* API: Ordinarily and Terminally deprecated
* Use-site: Not deprecated
* Deprecation warning: Ordinary warnings
*/
@SuppressWarnings("deprecation")
public static void m42() {
Box.deprecatedOrdinarily();
Box.deprecatedTerminally();
}
/**
* API: Ordinarily and Terminally deprecated
* Use-site: Not deprecated
* Deprecation warning: Removal warnings
*/
@SuppressWarnings("removal")
public static void m43() {
Box.deprecatedOrdinarily();
Box.deprecatedTerminally();
}
/**
* API: Ordinarily and Terminally deprecated
* Use-site: Not deprecated
* Deprecation warning: Removal warnings
*/
@SuppressWarnings({"deprecation", "removal"})
public static void m44() {
Box.deprecatedOrdinarily();
Box.deprecatedTerminally();
}
}
Listing 1-19A BoxTest Class That Uses Deprecated APIs and Suppresses Deprecation Warnings
您需要使用-Xlint:deprecation编译器标志来编译BoxTest类,因此编译器会发出弃用警告:
C:\Java9LanguageFeatures>javac -Xlint:deprecation ^
-d build\modules\jdojo.annotation ^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:20: warning: [deprecation]
deprecatedOrdinarily() in Box has been deprecated
Box.deprecatedOrdinarily();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:29: warning: [removal]
deprecatedTerminally() in Box has been deprecated
and marked for removal
Box.deprecatedTerminally();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:62: warning: [removal]
deprecatedTerminally() in Box has been deprecated
and marked for removal
Box.deprecatedTerminally();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:95: warning: [removal]
deprecatedTerminally() in Box has been deprecated
and marked for removal
Box.deprecatedTerminally();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:104: warning: [deprecation]
deprecatedOrdinarily() in Box has been deprecated
Box.deprecatedOrdinarily();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:105: warning: [removal]
deprecatedTerminally() in Box has been deprecated
and marked for removal
Box.deprecatedTerminally();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:116: warning: [removal]
deprecatedTerminally() in Box has been deprecated
and marked for removal
Box.deprecatedTerminally();
^
src\jdojo.annotation\classes\com\jdojo\annotation\
BoxTest.java:126: warning: [deprecation]
deprecatedOrdinarily() in Box has been deprecated
Box.deprecatedOrdinarily();
^
8 warnings
(命令中的“批注”后没有换行符和空格。)
回想一下,弃用警告是编译时警告。如果为您部署的应用程序编译的代码开始使用通常不推荐使用的 API,或者由于曾经有效的 API 已被最终不推荐使用并移除而生成运行时错误,您将不会收到任何警告。JDK9 和更高版本通过提供一个名为jdeprscan的静态分析工具来改善这种情况,该工具扫描编译后的代码,给出正在使用的不推荐使用的 API 的列表。目前,该工具仅报告了已弃用的 JDK API 的使用。如果您编译的代码使用了来自其他库(比如 Spring 或 Hibernate)或您自己的库的不推荐使用的 API,该工具将不会报告这些使用。
jdeprscan工具在JDK_HOME\bin目录中。使用该工具的一般语法如下:
jdeprscan [options] {dir|jar|class}
这里,[options]是零个或多个选项的列表。您可以指定一系列以空格分隔的目录、jar、完全限定的类名或类文件路径作为要扫描的参数。可用选项如下:
-
-l,–list -
–class-path <CLASSPATH> -
–for-removal -
–release <6|7|8|9|...|17> -
-v,–verbose -
–version -
–full-version -
-h,–help
–list选项列出了 Java SE 中一组不推荐使用的 API。使用此选项时,不应指定指定编译类位置的参数。
–class-path指定在扫描过程中用于查找依赖类的类路径。
–for-removal选项将扫描或列表限制为仅扫描那些不赞成删除的 API。
–release选项指定了 Java SE 版本,该版本在扫描期间提供了一组不推荐使用的 API。例如,要列出 JDK15 中所有不推荐使用的 API,您将使用如下工具:jdeprscan –list –release 15。
–verbose选项在扫描过程中打印附加信息。
–version和–full-version选项分别打印jdeprscan工具的缩略版和完整版。
–help选项打印关于jdeprscan工具的详细帮助信息。
清单 1-20 包含了一个JDeprScanTest类的代码。代码很简单。它的目的只是编译,而不是运行。运行它不会产生任何有趣的输出。它创建了两个线程。一个线程使用 thread 类的stop()方法停止,另一个线程使用Thread类的destroy()方法销毁。分别从 JDK 1.2 和 JDK 1.5 开始,stop()和destroy()方法已经被弃用。JDK9 最终弃用了destroy()方法,而它继续保留了通常弃用的stop()方法。我在下面的例子中使用这个类。
// JDeprScanTest.java
package com.jdojo.annotation;
public class JDeprScanTest {
public static void main(String[] args) {
Thread t = new Thread(() ->
System.out.println("Test"));
t.start();
t.stop();
Thread t2 = new Thread(() ->
System.out.println("Test"));
t2.start();
t2.destroy();
}
}
Listing 1-20A JDeprScanTest Class That Uses the Ordinarily Deprecated Method stop() and the Terminally Deprecated Method destroy() of the Thread Class
以下命令打印 JDK16 中所有不推荐使用的 API 的列表。该命令需要几秒钟时间开始打印结果,因为它扫描整个 JDK:
C:\Java9LanguageFeatures>jdeprscan --list --release 16
@Deprecated(since="16", forRemoval=true)
javax.management.relation.RoleStatus()
@Deprecated(since="9") interface
java.beans.AppletInitializer
...
以下命令打印 JDK16 中所有已过时的 API。也就是说,它打印所有已标记为在未来版本中删除的不推荐使用的 API:
C:\Java9LanguageFeatures>jdeprscan --list --for-removal ^
--release 16
@Deprecated(since="16", forRemoval=true)
javax.management.relation.RoleStatus()
...
以下命令打印 JDK8 中不推荐使用的所有 API 的列表:
C:\ Java9LanguageFeatures >jdeprscan --list --release 8
@Deprecated class javax.swing.text.TableView.TableCell
...
以下命令打印了 JDK16 中由java.lang.Thread类使用的不推荐使用的 API 列表:
C:\Java9LanguageFeatures>jdeprscan --release 16 ^
java.lang.Thread
class java/lang/Thread uses deprecated method
java/lang/Thread::resume()V (forRemoval=true)
注意,前面的命令没有打印出Thread类中不推荐使用的 API 的列表。更确切地说,它打印了使用这些不赞成使用的 API 的Thread类中的 API 列表。
以下命令列出了某个目录中不推荐使用的 JDK API 的所有用法:
C:\Java9LanguageFeatures>jdeprscan --release 16 ^
path/to/folder
class com/test/Jdk17 uses deprecated method
java/lang/Integer::<init>(I)V (forRemoval=true)
jdeprscan工具是一个静态分析工具,所以它将跳过不赞成使用的 API 的动态使用。例如,您可以使用反射调用不推荐使用的方法,该工具将在扫描过程中错过该方法。您还可以在由ServiceLoader加载的提供程序中调用不推荐使用的方法,这将被该工具忽略。
在 JDK9 之前,如果您使用import语句导入不推荐使用的构造,编译器会生成警告,即使您在不推荐使用的导入构造的所有使用位置上使用了@SuppressWarnings注解。如果您试图在代码中消除所有不赞成使用的警告,这是一件令人烦恼的事情。你就是无法摆脱它们,因为你无法注解import的陈述。JDK9 对此进行了改进,省略了对import语句的反对警告。
取消命名的编译时警告
SuppressWarnings注解类型用于隐藏指定的编译时警告。它声明了一个名为value的元素,其数据类型是一个String数组。让我们考虑一下SuppressWarningsTest类的代码,它在test()方法中使用了ArrayList<T>的原始类型。当您使用原始类型时,编译器会生成未检查的命名警告。见清单 1-21 。
// SuppressWarningsTest.java
package com.jdojo.annotation;
import java.util.ArrayList;
public class SuppressWarningsTest {
public void test() {
ArrayList list = new ArrayList();
list.add("Hello"); // The compiler issues an
// unchecked warning
}
}
Listing 1-21A Class That Will Generate Warnings When Compiled
使用以下命令编译带有生成未检查警告选项的SuppressWarningsTest类:
javac -Xlint:unchecked SuppressWarningsTest.java
com\jdojo\annotation\SuppressWarningsTest.java:10:
warning: [unchecked] unchecked call to add(E) as a
member of the raw type ArrayList
list.add("Hello");
^
where E is a type-variable
E extends Object declared in class ArrayList
1 warning
作为开发人员,有时您会意识到这种编译器警告,并且希望在编译代码时抑制它们。您可以通过在程序元素上使用@SuppressWarnings注解来实现这一点,方法是提供一个要取消的警告名称列表。例如,如果在类声明中使用它,则该类声明中所有方法的所有指定警告都将被取消。建议您在想要取消警告的最内层程序元素上使用此批注。
清单 1-22 在test()方法上使用了一个@SuppressWarnings注解。它指定了两个命名警告:“未检查”和“不推荐”test()方法不包含会生成“已弃用”警告的代码。这里包含它是为了向您展示,您可以使用一个SuppressWarnings注解来抑制多个命名警告。如果您使用前面显示的相同选项重新编译SuppressWarningsTest类,它不会生成任何编译器警告。
// SuppressWarningsTest.java
package com.jdojo.annotation;
import java.util.ArrayList;
public class SuppressWarningsTest {
@SuppressWarnings({"unchecked", "deprecation"})
public void test() {
ArrayList list = new ArrayList();
list.add("Hello"); // The compiler does not
// issue an unchecked warning
}
}
Listing 1-22The Modified Version of the SuppressWarningsTest Class
重写方法
java.lang.Override注解类型是标记注解类型。它只能用在方法上。它表示用该注解注解的方法覆盖了在其父类型中声明的方法。这对于开发人员避免导致程序逻辑错误的错别字非常有帮助。如果你想覆盖一个超类型中的方法,建议用一个@Override注解来注解被覆盖的方法。编译器将确保带注解的方法确实覆盖了超类型中的方法。如果带注解的方法没有覆盖超类型中的方法,编译器将生成一个错误。
考虑两个类,A和B。类别B继承自类别A。类B中的m1()方法覆盖了其超类A中的m1()方法。类B中的m1()方法上的注解@Override只是对这个意图做了一个声明。编译器验证该语句,并在以下情况下发现其为真:
public class A {
public void m1() {
}
}
public class B extends A {
@Override
public void m1() {
}
}
让我们考虑一下类别C:
// Won't compile because m2() does not override any method
public class C extends A {
@Override
public void m2() {
}
}
类C中的方法m2()有一个@Override注解。然而,在其超类A中没有m2()方法。方法m2()是类C中的一个新方法。编译器发现类C中的方法m2()没有覆盖任何超类方法,即使它的开发者已经指出了这一点。在这种情况下,编译器会生成一个错误。
声明功能接口
具有一个抽象方法声明的接口称为函数接口。以前,函数接口被称为 SAM(单一抽象方法)类型。编译器会验证所有用@FunctionalInterface标注的接口是否真的包含且只有一个抽象方法。如果用该注解注解的接口不起作用,就会产生编译时错误。在类、注解类型和枚举上使用此注解也是一个编译时错误。FunctionalInterface注解类型是标记注解。
下面的Runner接口声明使用了一个@FunctionalInterface注解。接口声明可以很好地编译:
@FunctionalInterface
public interface Runner {
void run();
}
下面的Job接口声明使用了一个@FunctionalInterface注解,这将产生一个编译时错误,因为Job接口声明了两个抽象方法,因此它不是一个函数接口:
@FunctionalInterface
public interface Job {
void run();
void abort();
}
下面的Test类声明使用了一个@FunctionalInterface注解,这将产生一个编译时错误,因为@FunctionalInterface注解只能在接口上使用:
@FunctionalInterface
public class Test {
public void test() {
// Code goes here
}
}
Note
一个只有一个抽象方法的接口总是一个函数接口,不管它是否有注解。注解的使用指示编译器验证接口确实是一个功能接口。
注解包
注解程序元素(如类和字段)是很直观的,因为您是在声明它们时对它们进行注解的。你如何注解一个包?包声明作为顶级类型声明的一部分出现在编译单元中。此外,相同的包声明在不同的编译单元中出现多次。问题出现了:如何以及在哪里注解一个包声明?
您需要创建一个名为package-info.java的文件,并将带注解的包声明放入其中。清单 1-23 显示了package-info.java文件的内容。当你编译package-info.java文件时,会创建一个类文件。
// package-info.java
@Version(major=1, minor=0)
package com.jdojo.annotation;
Listing 1-23Contents of a package-info.java File
您可能需要一些import语句来导入注解类型,或者您可以在package-info.java文件中使用注解类型的完全限定名。即使import语句出现在包声明之后,使用导入的类型也是可以的。在一个package-info.java文件中可以有如下内容:
// package-info.java
@com.jdojo.myannotations.Author("John Jacobs")
@Reviewer("Wally Inman")
package com.jdojo.annotation;
import com.jdojo.myannotations.Reviewer;
注解模块
您可以在module声明中使用注解。为此,java.lang.annotation.ElementType枚举有一个名为MODULE的值。如果在注解声明中使用MODULE作为目标类型,它允许在模块中使用注解类型。两个注解java.lang.Deprecated和java.lang.SuppressWarnings可用于模块声明,如下所示:
@Deprecated(since="1.2", forRemoval=true)
@SuppressWarnings("unchecked")
module com.jdojo.myModule {
// Module statements go here
}
当一个模块被弃用时,在requires中使用该模块,而不是在exports或opens语句中使用该模块,会导致发出警告。该规则基于这样一个事实,即如果模块M被弃用,需要得到弃用警告的模块用户将使用“requires M”语句。其他语句,如exports和opens都在不推荐使用的模块中。不推荐使用的模块不会导致针对模块内类型的使用发出警告。同样,如果在模块声明中取消了警告,则该取消适用于模块声明中的元素,而不适用于该模块中包含的类型。
Note
您不能注解单个模块语句。例如,您不能用一个@Deprecated注解来注解一个exports语句,表示导出的包将在未来的版本中被删除。在早期设计阶段,考虑并拒绝了该功能,理由是该功能将占用大量时间,而目前并不需要。如果需要,这可以在将来添加。
运行时访问注解
访问程序元素上的注解很容易。程序元素上的注解是 Java 对象。你只需要知道如何在运行时获得一个注解类型的对象的引用。允许您访问注解的程序元素实现了java.lang.reflect.AnnotatedElement接口。在AnnotatedElement接口中有几种方法可以让您访问程序元素的注解。此接口中的方法允许您检索程序元素上的所有注解、程序元素上所有声明的注解以及程序元素上指定类型的注解。我将展示一些使用这些方法的例子。下面的类实现了AnnotatedElement接口:
-
java.lang.Class -
java.lang.reflect.Executable -
java.lang.reflect.Constructor -
java.lang.reflect.Field -
java.lang.reflect.Method -
java.lang.reflect.Module -
java.lang.reflect.Parameter -
java.lang.Package -
java.lang.reflect.AccessibleObject
接口的方法被用来访问这些类型对象的注解。
Caution
非常重要的一点是,要在运行时访问一个注解类型,必须用带有runtime保留策略的Retention元注解对其进行注解。如果一个程序元素有多个注解,那么您将只能访问那些使用runtime作为保留策略的注解。
假设您有一个Test类,您想打印它的所有注解。下面的代码片段将打印Test类的类声明上的所有注解:
// Get the class object reference
Class<Test> cls = Test.class;
// Get all annotations on the class declaration
Annotation[] allAnns = cls.getAnnotations();
System.out.println("Annotation count: " + allAnns.length);
// Print all annotations
for (Annotation ann : allAnns) {
System.out.println(ann.toString());
}
批注接口的toString()方法返回批注的字符串表示。假设您想在Test类上打印Version注解。您可以这样做:
Class<Test> cls = Test.class;
// Get the instance of the Version annotation of Test
// class
Version v = cls.getAnnotation(Version.class);
if (v == null) {
System.out.println(
"Version annotation is not present.");
} else {
int major = v.major();
int minor = v.minor();
System.out.println("Version: major=" + major +
", minor=" + minor);
}
这段代码显示了您可以使用major()和minor()方法来读取Version注解的major和minor元素的值。它还展示了您可以声明一个注解类型的变量(例如Version v,它可以引用该注解类型的一个实例。注解类型的实例由 Java 运行时创建。永远不要使用new操作符创建注解类型的实例。
您将使用Version和Deprecated注解类型来注解您的程序元素,并在运行时访问这些注解。您还将注解一个包声明和一个方法声明。您将使用Version注解类型的代码,如清单 1-24 中所列。请注意,它使用了@Retention(RetentionPolicy.RUNTIME)注解,需要在运行时读取它的实例。
// Version.java
package com.jdojo.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR,
ElementType.METHOD, ElementType.MODULE,
ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
int major();
int minor();
}
Listing 1-24A Version Annotation Type
清单 1-25 显示了您需要保存在package-info.java文件中并与其他程序一起编译的代码。它注解了com.jdojo.annotation包。清单 1-26 包含了一个用于演示的类的代码,其中有一些注解。
// AccessAnnotation.java
package com.jdojo.annotation;
@Version(major=1, minor=0)
public class AccessAnnotation {
@Version(major=1, minor=1)
public void testMethod1() {
// Code goes here
}
@Version(major=1, minor=2)
@Deprecated
public void testMethod2() {
// Code goes here
}
}
Listing 1-26AccessAnnotation Class Has Some Annotations, Which Will Be Accessed at Runtime
// package-info.java
@Version(major=1, minor=0)
package com.jdojo.annotation;
Listing 1-25Contents of the package-info.java File
清单 1-27 是演示如何在运行时访问注解的程序。它的输出显示您能够成功地读取在AccessAnnotation类中使用的所有注解。printAnnotations()方法访问注解。它接受一个AnnotatedElement类型的参数,并打印其参数的所有注解。如果注解是Version注解类型,它打印其主要和次要版本的值。
// AccessAnnotationTest.java
package com.jdojo.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
public class AccessAnnotationTest {
public static void main(String[] args) {
// Read annotations on the class declaration
Class<AccessAnnotation> cls =
AccessAnnotation.class;
System.out.println("Annotations for class: " +
cls.getName());
printAnnotations(cls);
// Read annotations on the package declaration
Package p = cls.getPackage();
System.out.println("Annotations for package: " +
p.getName());
printAnnotations(p);
// Read annotations on the methods declarations
System.out.println("Method annotations:");
Method[] methodList = cls.getDeclaredMethods();
for (Method m : methodList) {
System.out.println("Annotations for method: " +
m.getName());
printAnnotations(m);
}
}
public static void printAnnotations(
AnnotatedElement programElement) {
Annotation[] annList = programElement.
getAnnotations();
for (Annotation ann : annList) {
System.out.println(ann);
if (ann instanceof Version) {
Version v = (Version) ann;
int major = v.major();
int minor = v.minor();
System.out.println(
"Found Version annotation: "
+ "major=" + major +
", minor=" + minor);
}
}
System.out.println();
}
}
Annotations for class:
com.jdojo.annotation.AccessAnnotation
@com.jdojo.annotation.Version(major=1, minor=0)
Found Version annotation: major=1, minor=0
Annotations for package: com.jdojo.annotation
@com.jdojo.annotation.Version(major=1, minor=0)
Found Version annotation: major=1, minor=0
Method annotations:
Annotations for method: testMethod1
@com.jdojo.annotation.Version(major=1, minor=1)
Found Version annotation: major=1, minor=1
Annotations for method: testMethod2
@com.jdojo.annotation.Version(major=1, minor=2)
Found Version annotation: major=1, minor=2
@java.lang.Deprecated(forRemoval=false, since="")
Listing 1-27Using the AccessAnnotationTest Class to Access Annotations
访问可重复注解的实例略有不同。回想一下,可重复注解有一个包含注解类型的同伴。例如,您声明了一个ChangeLogs注解类型,它是ChangeLog可重复注解类型的包含注解类型。您可以使用注解类型或包含注解类型来访问重复的注解。使用getAnnotationsByType()方法,向其传递可重复注解类型的类引用,以获取数组中可重复注解的实例。使用getAnnotation()方法,向其传递包含注解类型的类引用,以获取可重复注解的实例,作为其包含注解类型的实例。
清单 1-28 包含一个RepeatableAnnTest类的代码。类声明已经用ChangeLog注解注解了两次。main()方法使用这两种方法访问类声明中重复的注解。
// RepeatableAnnTest.java
package com.jdojo.annotation;
@ChangeLog(date = "09/18/2017",
comments = "Declared the class")
@ChangeLog(date = "10/22/2017",
comments = "Added the main() method")
public class RepeatableAnnTest {
public static void main(String[] args) {
Class<RepeatableAnnTest> mainClass =
RepeatableAnnTest.class;
Class<ChangeLog> annClass = ChangeLog.class;
// Access annotations using the ChangeLog type
System.out.println("Using the ChangeLog type...");
ChangeLog[] annList = mainClass.
getAnnotationsByType(ChangeLog.class);
for (ChangeLog log : annList) {
System.out.println("Date=" + log.date() +
", Comments=" + log.comments());
}
// Access annotations using the ChangeLogs
// containing annotation type
System.out.println(
"\nUsing the ChangeLogs type...");
Class<ChangeLogs> containingAnnClass =
ChangeLogs.class;
ChangeLogs logs = mainClass.getAnnotation(
containingAnnClass);
for (ChangeLog log : logs.value()) {
System.out.println("Date=" + log.date() +
", Comments=" + log.comments());
}
}
}
Using the ChangeLog type...
Date=09/18/2017, Comments=Declared the class
Date=10/22/2017, Comments=Added the main() method
Using the ChangeLogs type...
Date=09/18/2017, Comments=Declared the class
Date=10/22/2017, Comments=Added the main() method
Listing 1-28Accessing Instances of Repeatable Annotations at Runtime
演变注解类型
注解类型可以在不破坏使用它的现有代码的情况下发展。如果向注解类型添加新元素,则需要提供其默认值。注解的所有现有实例都将使用新元素的默认值。如果在没有为元素指定默认值的情况下向现有批注类型添加新元素,使用该批注的代码将会中断。
源代码级别的注解处理
这一部分是为有经验的程序员准备的。如果你是第一次学习 Java,你可以跳过这一节。我们将详细讨论如何开发注解处理器,以便在编译 Java 程序时在源代码级别处理注解。
Note
华盛顿大学开发了一个 Checker 框架,它包含了许多程序中使用的注解。它还附带了许多注解处理器。您可以从 https://checkerframework.org/ 下载 Checker 框架。它包含使用不同类型处理器的教程和如何创建自己的处理器的教程。
Java 允许您在运行时和编译时处理注解。您已经看到了如何在运行时处理注解。现在,我简要地讨论如何在编译时(或者在源代码级别)处理注解。
为什么要在编译时处理注解?编译时处理注解提供了多种可能性,可以在应用程序开发过程中帮助 Java 程序员。它也极大地帮助了 Java 工具的开发者。例如,样板代码和配置文件可以基于源代码中的注解生成;基于注解的定制规则可以在编译时进行验证,等等。
编译时的注解处理是一个两步过程。首先,您需要编写一个定制的注解处理器。其次,您需要使用javac命令行实用工具。您需要使用–processor-modulepath选项指定自定义注解处理器到javac编译器的模块路径。以下命令编译 Java 源文件MySourceFile.java:
javac --processor-module-path <path> MySourceFile.java
使用-proc选项,javac命令让您指定是否要处理注解和/或编译源文件。您可以使用-proc选项作为-proc:none或-proc:only。-proc:none选项不执行标注处理。它只编译源文件。-proc:only选项只执行注解处理,跳过源文件编译。如果在同一个命令中指定了-proc:none和-processor选项,则-processor选项被忽略。以下命令使用定制处理器处理源文件MySourceFile.java中的注解:MyProcessor1和MyProcessor2。它不编译MySourceFile.java文件中的源代码:
javac -proc:only --processor-module-path <path> ^
MySourceFile.java
要查看运行中的编译时注解处理,您必须使用javax.annotation.processing包中的类编写一个注解处理器,该包在java.compiler模块中。
在编写自定义注解处理器时,您经常需要访问源代码中的元素,例如,类名及其修饰符、方法名及其返回类型等。您需要使用javax.lang.model包及其子包中的类来处理源代码的元素。在您的例子中,您将为您的@Version注解编写一个注解处理器。它将验证源代码中使用的所有@Version注解,以确保Version的major和minor值总是零或大于零。例如,如果源代码中使用了@Version(major=-1, minor=0),注解处理器会打印一条错误消息,因为版本的主值是负的。
注解处理器是一个类的对象,它实现了Processor接口。AbstractProcessor类是一个抽象注解处理器,它为Processor接口的所有方法提供了一个默认实现,除了为process()方法提供了一个实现。默认实现在大多数情况下都可以。要创建自己的处理器,您需要从AbstractProcessor类继承您的处理器类,并为process()方法提供一个实现。如果AbstractProcessor类不能满足您的需要,您可以创建自己的处理器类,它实现了Processor接口。让我们调用您的处理器类VersionProcessor,它继承自AbstractProcessor类,如下所示:
public class VersionProcessor extends AbstractProcessor {
// Code goes here
}
注解处理器对象由编译器使用无参数构造函数进行实例化。您的处理器类必须有一个无参数构造函数,以便编译器可以实例化它。您的VersionProcessor类的默认构造函数将满足这一要求。
下一步是向处理器类添加两条信息。第一个问题是这个处理器支持哪种注解处理。您可以在类级别使用@SupportedAnnotationTypes注解来指定支持的注解类型。下面的代码片段显示了VersionProcessor支持处理com.jdojo.annotation.Version注解类型:
@SupportedAnnotationTypes({"com.jdojo.annotation.Version"})
public class VersionProcessor extends AbstractProcessor {
// Code goes here
}
您可以单独使用星号(),也可以将星号作为受支持注解类型的注解名称的一部分。星号用作通配符。例如,“com.jdojo.”表示名称以“com.jdojo”开头的任何批注类型。仅星号(“”)表示所有注解类型。请注意,当星号用作名称的一部分时,名称的形式必须是PartialName.*。例如,“com”和“com。*jdojo "是受支持的批注类型中星号的无效用法。您可以使用SupportedAnnotationTypes注解传递多个支持的注解类型。下面的代码片段显示处理器支持处理com.jdojo.Ann1注解和任何名称以com.jdojo.annotation开头的注解:
@SupportedAnnotationTypes({"com.jdojo.Ann1",
"com.jdojo.annotation.*"})
您需要使用一个@SupportedSourceVersion注解来指定您的处理器支持的最新源代码版本。以下代码片段将源代码版本 17 指定为VersionProcessor类支持的源代码版本:
@SupportedAnnotationTypes({"com.jdojo.annotation.Version"})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class VersionProcessor extends AbstractProcessor {
// Code goes here
}
下一步是在处理器类中提供process()方法的实现。注解处理是循环执行的。RoundEnvironment接口的一个实例代表一轮。javac编译器通过传递处理器声明支持的所有注解和一个RoundEnvironment对象来调用处理器的process()方法。process()方法的返回类型是布尔值。如果它返回true,传递给它的注解就被认为是处理器所要求的。所声明的注解不会被传递给其他处理器。如果它返回false,传递给它的注解被认为没有被声明,其他处理器将被要求处理它们。下面的代码片段展示了process()方法的框架:
public boolean process(Set<? extends TypeElement>
annotations, RoundEnvironment roundEnv) {
// The processor code goes here
}
您在process()方法中编写的代码取决于您的需求。在您的例子中,您想要查看源代码中每个@Version注解的major和minor值。如果它们中的任何一个小于零,您希望打印一条错误消息。为了处理每个Version注解,您将遍历传递给process()方法的所有Version注解实例,如下所示:
for (TypeElement currentAnnotation : annotations) {
// Code to validate each Version annotation goes here
}
您可以使用TypeElement接口的getQualifiedName()方法获得注解的全限定名称:
Name qualifiedName = currentAnnotation.getQualifiedName();
// Check if it is a Version annotation
if (qualifiedName.contentEquals(
"com.jdojo.annotation.Version")) {
// Get Version annotation values to validate
}
一旦确定有了一个Version注解,就需要从源代码中获取它的所有实例。要从源代码中获取信息,需要使用RoundEnvironment对象。下面的代码片段将获得源代码的所有元素(例如,类、方法、构造函数等。)用一个Version注解进行了注解:
Set<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(currentAnnotation);
此时,您需要遍历所有用Version注解标注的元素;获取出现在它们上面的Version注解的实例;并验证major和minor元素的值。您可以按如下方式执行此逻辑:
for (Element element : annotatedElements) {
Version v = element.getAnnotation(Version.class);
int major = v.major();
int minor = v.minor();
if (major < 0 || minor < 0) {
// Print the error message here
}
}
您可以使用Messager的printMessage()方法打印错误信息。processingEnv是在AbstractProcessor类中定义的一个实例变量,您可以在处理器内部使用它来获取Messager对象引用,如下所示。如果您将源代码元素的引用传递给printMessage()方法,您的消息将被格式化为包含源代码文件名和该元素源代码中的行号。printMessage()方法的第一个参数表示消息的类型。您可以使用Kind.NOTE和Kind.WARNING作为第一个参数来分别打印注解和警告。
String errorMsg = "Version cannot be negative. major=" +
major + " minor=" + minor;
Messager messager = this.processingEnv.getMessager();
messager.printMessage(Kind.ERROR, errorMsg, element);
最后,您需要从process()方法中返回true或false。如果处理器返回true,这意味着它声明了传递给它的所有注解。否则,这些注解被认为是无人认领的,它们将被传递给其他处理器。通常,注解处理器应该封装在一个单独的模块中。清单 1-29 包含了一个jdojo.annotation.processor模块的声明,该模块包含了用于Version注解类型的名为VersionProcessor的注解处理器,如清单 1-30 所示。
// module-info.java
module jdojo.annotation.processor {
exports com.jdojo.annotation.processor;
requires jdojo.annotation;
requires java.compiler;
provides javax.annotation.processing.Processor
with
com.jdojo.annotation.processor.VersionProcessor;
}
Listing 1-29The Declaration for a jdojo.annotation.processor Module
该模块读取jdojo.annotation模块,因为它使用了VersionProcessor类中的Version注解类型。它读取java.compiler模块以使用注解处理器相关的类型。注意模块声明中 provides 语句的使用。Java 将在 provides 语句的with子句中提到的处理器模块路径上加载所有注解处理器。该语句指定了VersionProcessor类为Processor服务接口提供了一个实现。有关provides声明和实现服务的更多详细信息,请参考第七章。
// VersionProcessor.java
package com.jdojo.annotation.processor;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
@SupportedAnnotationTypes({
"com.jdojo.annotation.Version"})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class VersionProcessor extends AbstractProcessor {
// A no-args constructor is required for an
// annotation processor
public VersionProcessor() {
}
@Override
public boolean process(Set<? extends TypeElement>
annotations, RoundEnvironment roundEnv) {
// Process all annotations
for (TypeElement currentAnnotation: annotations) {
Name qualifiedName = currentAnnotation.
getQualifiedName();
// check if it is a Version annotation
if (qualifiedName.contentEquals(
"com.jdojo.annotation.Version" )) {
// Look at all elements that have Version
// annotations
Set<? extends Element> annotatedElements;
annotatedElements = roundEnv.
getElementsAnnotatedWith(
currentAnnotation);
for (Element element: annotatedElements) {
Version v = element.getAnnotation(
Version.class);
int major = v.major();
int minor = v.minor();
if (major < 0 || minor < 0) {
// Print the error message
String errorMsg =
"Version cannot be negative." +
" major=" + major +
" minor=" + minor;
Messager messager = this.
processingEnv.getMessager();
messager.printMessage(Kind.ERROR,
errorMsg, element);
}
}
}
}
return true;
}
}
Listing 1-30An Annotation Processor to Process Version Annotations
现在您有了一个注解处理器。是时候看看它的实际效果了。您需要有一个在Version注解中对major和minor元素使用无效值的源代码。您将把源代码放在一个名为jdojo.annotation.test的模块中,如清单 1-31 所示。清单 1-32 中的VersionProcessorTest类使用了三次Version注解。它为类本身和方法m2()的major和minor元素使用负值。当您为VersionProcessorTest类编译源代码时,处理器应该会捕捉到这两个错误。
// VersionProcessorTest.java
package com.jdojo.annotation.test;
@Version(major = -1, minor = 2)
public class VersionProcessorTest {
@Version(major = 1, minor = 1)
public void m1() {
}
@Version(major = -2, minor = 1)
public void m2() {
}
}
Listing 1-32A Test Class to Test VersionProcessor
// module-info.java
module jdojo.annotation.test {
exports com.jdojo.annotation.test;
requires jdojo.annotation;
}
Listing 1-31The Declaration of a jdojo.annotation.test Module
要查看处理器的运行情况,您需要运行以下命令。您需要使用–processor-module-path选项为VersionProcessor类的模块指定路径。注解处理器所依赖的模块也应该在处理器模块路径中指定。当命令运行时,编译器将自动发现VersionProcessor作为注解处理器,并将所有@Version实例传递给这个处理器。输出显示两个错误,包括源文件名和在源文件中发现错误的行号:
C:\Java9LanguageFeatures>javac --module-path ^
dist\jdojo.annotation.jar ^
--processor-module-path ^
dist\jdojo.annotation.processor.jar;
dist\jdojo.annotation.jar ^
-d build\modules\jdojo.annotation.test
src\jdojo.annotation.test\classes\module-info.java
src\jdojo.annotation.test\classes\com\jdojo\annotation\
test\VersionProcessorTest.java
src\jdojo.annotation.test\classes\com\jdojo\annotation\
test\VersionProcessorTest.java:7:
error: Version cannot be negative. major=-1 minor=2
public class VersionProcessorTest {
^
src\jdojo.annotation.test\classes\com\jdojo\annotation\
test\VersionProcessorTest.java:13:
error: Version cannot be negative. major=-2 minor=1
public void m2() {
^
2 errors
(在“dist \ jdo jo . annotation . processor . jar”之后没有换行符和空格。)
摘要
注解是 Java 中的类型。它们用于将信息与 Java 程序中程序元素或类型使用的声明相关联。使用注解不会改变程序的语义。
注解只能在源代码、类文件或运行时使用。它们的可用性由声明注解类型时指定的保留策略控制。
有两种类型的注解:常规注解或简单注解和元注解。注解用于注解程序元素,而元注解用于注解其他注解。当您声明一个注解时,您可以指定它的目标,即它可以注解的程序元素的类型。注解可以在同一个元素上重复。
Java 库包含了许多可以在 Java 程序中使用的注解类型,如Deprecated、Override、SuppressWarnings、FunctionalInterface等。是一些常用的注解类型。它们有编译器支持,这意味着如果用这些注解标注的程序元素不符合特定的规则,编译器会生成错误。
Java 允许您编写注解处理器,这些处理器可以插入到 Java 编译器中,以便在编译 Java 程序时处理注解。您可以编写处理器来实现基于注解的定制规则。
Java 中的弃用是提供 API 生命周期信息的一种方式。放弃一个 API 告诉它的用户迁移出去,因为这个 API 使用起来很危险,有更好的替代品存在,或者它将在未来的版本中被删除。使用不推荐使用的 API 会生成编译时不推荐使用的警告。@deprecated Javadoc 标签和@Deprecated注解一起使用来取代 API 元素,如模块、包、类型、构造函数、方法、字段、参数和局部变量。该注解在运行时被保留。
Deprecated注解类型包含since和forRemoval作为元素。since元素默认为空字符串。它的值表示 API 元素被弃用的 API 版本。forRemoval元素的类型为布尔型,默认为false。它的值true表示 API 元素将在未来的版本中被移除。
编译器(从 JDK9 开始)根据@Deprecated注解的forRemoval元素的值生成两种类型的弃用警告:当forRemoval=false时的普通弃用警告和针对forRemoval=true的移除警告。
您需要使用@SuppressWarnings("deprecation")来抑制普通警告,使用@SuppressWarnings("removal")来抑制删除警告,使用@SuppressWarnings({"deprecation", "removal"})来抑制这两种类型的警告。仅导入不推荐使用的构造,而不实际使用它,不会生成不推荐使用警告。
练习
练习 1
什么是注解?你如何申报它们?
练习 2
什么是元注解?
运动 3
注解类型和注解实例之间有什么区别?
演习 4
可以从另一个注解类型继承一个注解类型吗?
锻炼 5
什么是标记注解?描述它们的用途。说出 Java SE API 中两个可用的标记注解。
锻炼 6
命名其实例用于注解重写方法的注解类型。这个注解类型的完全限定名是什么?
锻炼 7
批注类型声明中的方法允许哪些返回类型?
运动 8
声明一个名为Table的注解类型。它包含一个名为name的String元素。唯一元素没有任何默认值。此批注只能用于类。它的实例应该在运行时可用。
演习 9
下面的注解类型声明有什么问题?
public @interface Version extends BasicVersion {
int extended();
}
运动 10
下面的注解类型声明有什么问题?
public @interface Author {
void name(String firstName, String lastName);
}
简述以下内置元注解的使用:Target、Retention、Inherited、Documented、Repeatable和Native。
演习 11
声明一个名为ModuleOwner的注解类型,它包含一个元素name,该元素属于String类型。类型的实例应该只保留在源代码中,并且应该只在模块声明中使用。
运动 12
声明一个名为Author的可重复注解类型。它包含两个String类型的元素:firstName和lastName。这个注解可以用在类型、方法和构造函数上。它的实例应该在运行时可用。将Author注解类型包含的注解类型命名为Authors。
运动 13
你用什么注解类型来反对你的 API?描述这种注解类型的所有元素。
运动 14
你用什么类型的注解来注解一个函数接口?
运动 15
你如何注解一个包?
演习 16
创建一个名为Owner的注解类型。它应该有一个String类型的元素name。它的实例应该在运行时保留。它应该是可重复的。它应该只用于类型、方法、构造函数和模块。在com.jdojo.annotation.exercises包中创建一个名为jdojo.annotation.test的模块和一个名为Test的类。向类中添加构造函数和方法。用Owner注解类型注解类、它的模块、构造函数和方法。向Test类添加一个main()方法,并编写代码来访问和打印这些Owner注解实例的详细信息。
演习 17
考虑以下名为Status的注解类型声明:
public @interface Status {
boolean approved() default false;
String approvedBy();
}
稍后,您需要向Status注解类型添加另一个元素。
修改注解的声明,以包含一个名为approvedOn的新元素,它属于String类型。新元素将包含 ISO 格式的日期,其默认值可以设置为“1900-01-01”。
演习 18
考虑下面名为LuckyNumber的注解类型的声明:
public @interface LuckyNumber {
int[] value() default {19};
}
LuckyNumber注解类型的下列哪种用法是无效的?解释你的答案。
-
@LuckyNumber -
@LuckyNumber({}) -
@LuckyNumber(10) -
LuckyNumber({8, 10, 19, 28, 29, 26}) -
LuckyNumber(value={8, 10, 19, 28, 29, 26}) -
@LuckyNumber(null)
演习 19
给定一个LuckyNumber注解类型,下面的变量声明是否有效?
LuckNumber myLuckNumber = null;
运动 20
考虑下面对一个jdojo.annotation.exercises模块的声明:
module jdojo.annotation.exercises {
exports com.jdojo.annotation.exercises;
}
该模块从版本 1.0 开始就存在。该模块已被弃用,将在下一版本中删除。注解模块声明以反映这些信息。