文章目录
- 第二十三章 注解
- 1. 基本语法
- 1.1 基本语法
- 1.2 定义注解
- 1.3 元注解
- 2. 编写注解处理器
- 2.1 编写注解处理器
- 2.2 注解元素
- 2.3 默认值限制
- 3. 使用javac处理注解
- 4. 基于注解的单元测试
- 5. 本章小结
第二十三章 注解
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。
通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下有下优势:简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。
注解的语法十分简单,主要是在现有语法中添加 @ 符号。
1. 基本语法
1.1 基本语法
使用 @Test 对 testExecute() 进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有 @Test 注解的定义。
package annotations;
import onjava.atunit.Test;
public class Testable {
public void execute() {
System.out.println("Executing..");
}
@Test
void testExecute() { execute(); }
}
1.2 定义注解
注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。
package onjava.atunit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
@Target 定义你的注解可以应用在哪里(例如是方法还是字段)。 @Retention 定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。
不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 @Test 就是标记注解。
下面是一个简单的注解,我们可以用它来追踪项目中的用例。
package annotations;
import java.util.List;
public class PasswordUtils {
// 注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 @UseCase 声明之后的括号内。
@UseCase(id = 47, description =
"Passwords must contain at least one numeric")
public boolean validatePassword(String passwd) {
return (passwd.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)
public String encryptPassword(String passwd) {
return new StringBuilder(passwd)
.reverse().toString();
}
@UseCase(id = 49, description =
"New passwords can't equal previously used ones")
public boolean checkForNewPassword(
List<String> prevPasswords, String passwd) {
return !prevPasswords.contains(passwd);
}
}
1.3 元注解
Java 语言中目前有 5 种标准注解:
-
@Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
-
@Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
-
@SuppressWarnings:关闭不当的编译器警告信息。
-
@SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
-
@FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口
以及 5 种元注解。元注解用于注解其他的注解
注解 | 解释 |
---|---|
@Target | 表示注解可以用于哪些地方。可能的 ElementType 参数包括: CONSTRUCTOR:构造器的声明 FIELD:字段声明(包括 enum 实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或者 enum 声明 |
@Retention | 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: SOURCE:注解将被编译器丢弃 SOURCE:注解将被编译器丢弃 CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。 RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信 息。 |
@Documented | 将此注解保存在 Javadoc 中 |
@Inherited | 允许子类继承父类的注解 |
@Repeatable | 允许一个注解可以被使用一次或者多次 |
2. 编写注解处理器
2.1 编写注解处理器
如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。
下面是一个非常简单的注解处理器,我们用它来读取被注解的 PasswordUtils 类,并且使用反射机制来寻找 @UseCase 标记。
package annotations;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for (Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if (uc != null) {
System.out.println("Found Use Case " +
uc.id() + "\n " + uc.description());
useCases.remove(Integer.valueOf(uc.id()));
}
}
useCases.forEach(i ->
System.out.println("Missing use case " + i));
}
public static void main(String[] args) {
List<Integer> useCases = IntStream.range(47, 51).boxed().collect(Collectors.toList());
trackUseCases(useCases, PasswordUtils.class);
}
}
输出:
Found Use Case 47
Passwords must contain at least one numeric
Found Use Case 49
New passwords can't equal previously used ones
Found Use Case 48
no description
Missing use case 50
2.2 注解元素
在 UseCase.java 中定义的 @UseCase 的标签包含 int 元素 id 和 String 元素 description。注解元素可用的类型如下所示:
-
所有基本类型(int、float、boolean等)
-
String
-
Class
-
enum
-
Annotation
-
以上类型的数组
2.3 默认值限制
编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。
这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。
package annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
int id() default -1;
String description() default "";
}
注解不支持继承
3. 使用javac处理注解
通过 javac,你可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。
每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。
4. 基于注解的单元测试
单元测试是对类中每个方法提供一个或者多个测试的一种事件,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在 Java 中,最著名的单元测试工具就是 JUnit。JUnit 4 版本已经包含了注解。
5. 本章小结
注解是 Java 引入的一项非常受欢迎的补充,它提供了一种结构化,并且具有类型检查能力的新途径,从而使得你能够为代码中加入元数据,而且不会导致代码杂乱并难以阅读。
而 Javadoc 中的 @deprecated 被 @Deprecated 注解所替代的事实也说明,与注释性文字相比,注解绝对更适用于描述类相关的信息。
Java 提供了很少的内置注解。这意味着如果你在别处找不到可用的类库,那么就只能自己创建新的注解以及相应的处理器。通过将注解处理器链接到 javac,你可以一步完成编译新生成的文件,简化了构造过程。
API 的提供方和框架将会将注解作为他们工具的一部分。通过 @Unit 系统,我们可以想象,注解会极大的改变我们的 Java 编程体验。
(图网,侵删)