一、Java注解(Annotation)简介
从 Java 5 版本之后可以在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),是 Java 平台中非常重要的一部分。注解都是 @ 符号开头的,例如:在学习方法重写时使用过的 @Override 注解。同 Class 和 Interface 一样,注解也属于一种类型。
Annotation 可以翻译为“注解”或“注释”,一般翻译为“注解”,
- 注解并不能改变程序的运行结果,也不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有的注解可以在运行时读写字节码文件信息。
注解即一种描述数据的数据。所以可以说注解就是源代码的元数据。例如以下代码:
@Override
public String toString() {
return "helloworld";
}
上面的代码重写了 Object 类的 toString() 方法并使用了 @Override 注解。如果不使用 @Override 注解标记代码,程序也能够正常执行。那么这么写有什么好处吗?事实上,使用 @Override 注解就相当于告诉编译器这个方法是一个重写方法,如果父类中不存在该方法,编译器便会报错,提示该方法没有重写父类中的方法。这样可以防止不小心拼写错误造成麻烦。
注解常见的作用有以下几种:
- 生成帮助文档。这是最常用的,也是 Java 最早提供的注解。常用的有 @see、@param 和 @return 等;
- 跟踪代码依赖性,实现替代配置文件功能。比较常见的是 Spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;
- 在编译时进行格式检查。如把 @Override 注解放在方法前,如果这个方法并不是重写了父类方法,则编译时就能检查出。
无论是哪一种注解,本质上都一种数据类型,是一种接口类型。到 Java 8 为止 Java SE 提供了 11 个内置注解。其中有 5 个是基本注解,它们来自于 java.lang 包。有 6 个是元注解,它们来自于 java.lang.annotation 包,自定义注解会用到元注解。(元注解就是负责注解其他的注解)
基本注解包括:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs 和 @FunctionalInterface。
二、Java中常见注解
2.1.Java @Override注解
Java 中 @Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。使用 @Override 注解示例代码如下:
public class User implements Serializable{
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
说明:
- @Override 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。
- 如果代码中方法前面不加 @Override 注解,即便是方法编辑错误了,编译器也不会有提示(有@Override注解,如果有错误,编译的时候不会通过,报异常)。这时 Object 父类的 toString() 方法并没有被重写,将会引起程序出现 Bug(缺陷)。
2.2.Java @Deprecated注解
Java 中 @Deprecated 可以用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时。当其他程序使用已过时的元素时,编译器将会给出警告。
@Deprecated
public class User implements Serializable{
@Deprecated
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
@Deprecated
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
上述 User类、成员变量 username 和 setName方法都被 @Deprecated 注解。在 idea 中这些被注解的 API 都会被画上删除线。示例代码如下。
2.3.Java @SuppressWarnings:抑制编译器警告
Java 中的 @SuppressWarnings 注解指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素。例如,使用 @SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。
如果确认程序中的警告没有问题,可以不用理会。通常情况下,如果程序中使用没有泛型限制的集合将会引起编译器警告,为了避免这种编译器警告,可以使用 @SuppressWarnings 注解消除这些警告。注解的使用有以下三种:
- 抑制单类型的警告:@SuppressWarnings("unchecked")
- 抑制多类型的警告:@SuppressWarnings("unchecked","rawtypes")
- 抑制所有类型的警告:@SuppressWarnings("unchecked")
抑制警告的关键字如下表所示。
关键字 | 用途 |
all | 抑制所有警告 |
boxing | 抑制装箱、拆箱操作时候的警告 |
cast | 抑制映射相关的警告 |
dep-ann | 抑制启用注释的警告 |
deprecation | 抑制过期方法警告 |
fallthrough | 抑制在 switch 中缺失 breaks 的警告 |
finally | 抑制 finally 模块没有返回的警告 |
hiding | 抑制相对于隐藏变量的局部变量的警告 |
incomplete-switch | 忽略不完整的 switch 语句 |
nls | 忽略非 nls 格式的字符 |
null | 忽略对 null 的操作 |
rawtypes | 使用 generics 时忽略没有指定相应的类型 |
restriction | 抑制禁止使用劝阻或禁止引用的警告 |
serial | 忽略在 serializable 类中没有声明 serialVersionUID 变量 |
static-access | 抑制不正确的静态访问方式警告 |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 |
unchecked | 抑制没有进行类型检查操作的警告 |
unqualified-field-access | 抑制没有权限访问的域的警告 |
unused | 抑制没被使用过的代码的警告 |
使用 @SuppressWarnings 注解示例代码如下:
public class Test02 {
//抑制 过时方法或者类
@SuppressWarnings({ "deprecation" })
public static void main(String[] args) {
User user = new User("admin","123456");
user.setUsername("root");
System.out.println(user.toString());
//抑制 创建集合没有指定泛型的异常,all表示所有警告
@SuppressWarnings({"all"})
ArrayList objects = new ArrayList();
}
}
在显示如下图所示:
上述代码使用 @SuppressWarnings({ "deprecation" }) 注解了 main 方法。在之前案例中的 User 代码中,这些 API 已经过时了,所以代码有编译警告,但是在使用了 @SuppressWarnings 注解之后会发现程序代码的警告没有了。
2.4.Java @SafeVarargs注解
@SafeVarargs在JDK 7中引入,主要目的是处理可变长参数中的泛型,此注解告诉编译器:在可变参数中的泛型是类型安全的。可变参数是使用数组存储的,而数组和泛型不能很好的混合使用。
public class HelloWorld {
public static void main(String[] args) {
//传递可变参数。参数是泛型集合
display(10,23,12);
// 传递可变参数,参数是非泛型集合
display("hello",230,12);//会有编译警告
}
public static <T> void display(T... arrays){
for (T array : arrays) {
System.out.println(array.getClass().getName()+":"+array);
}
}
}
上面代码声明了一种可变参数方法 display,display 方法参数个数可以变化,它可以接受不确定数量的相同类型的参数。可以通过在参数类型名后面加入...
的方式来表示这是可变参数。可变参数方法中的参数类型相同,为此声明参数是需要指定泛型。
但是调用可变参数方法时,应该提供相同类型的参数,代码第一次调用时没有警告,而代码第二行调用时则会发生警告,这个警告是 unchecked(未检查不安全代码,参数化变量类型可能造成堆污染),就是因为将非泛型变量赋值给泛型变量所发生的。
可用 @SafeVarargs 注解抑制编译器警告,修改代码如下:
public class HelloWorld {
public static void main(String[] args) {
// 传递可变参数,参数是泛型集合
display(10, 20, 30);
// 传递可变参数,参数是非泛型集合
display("10", 20, 30); // 会有编译警告
}
@SafeVarargs
public static <T> void display(T... array) {
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}
}
上述代码在可变参数 display 前添加了 @SafeVarargs 注解,当然也可以使用 @SuppressWarnings("unchecked") 注解,但是两者相比较来说 @SafeVarargs 注解更适合。
注意:@SafeVarargs注解不适用于非 static 或非 final 声明的方法,对于未声明为 static 或 final 的方法,如果要抑制 unchecked 警告,可以使用 @SuppressWarnings 注解。
2.5.Java @FunctionalInterface注解
@FunctionalInterface是Java 8中引入的注解,用于标识一个接口是否为函数式接口。函数式接口指的是只有一个抽象方法的接口,它可以被Lambda表达式或方法引用所使用。
@FunctionalInterface可以帮助我们在编译期发现不符合函数式接口规范的接口,并避免在后续使用Lambda表达式时出现错误。如果一个接口被标记为@FunctionalInterface,但其中有多个抽象方法,编译器会提示错误。
@FunctionalInterface
interface MyFunctionalInterface {
void doSomething();
}
上面的代码中,MyFunctionalInterface是一个函数式接口,因为它只有一个抽象方法doSomething()。如果我们在该接口中添加另外一个抽象方法,编译器会提示错误。
需要:
- @FunctionalInterface注解不是必需的,只是用于帮助开发者识别函数式接口的工具之一。
- 如果一个接口只有一个抽象方法,即使没有使用@FunctionalInterface注解,它仍然是一个函数式接口。
三、Java自定义注解
Java 的基本注解和元注解如果不能满足你的需求,可以自定义注解。声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。定义注解与定义接口非常像,如下代码可定义一个简单形式的注解类型。
public @interface Test11 {
}
上述代码声明了一个 Test 11 注解。默认情况下,注解可以在程序的任何地方使用,通常用于修饰类、接口、方法和变量等。
- 定义注解和定义类相似,注解前面的访问修饰符和类一样有两种,分别是公有访问权限(public)和默认访问权限(默认不写)。
- 一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的注解。且源程序文件命名和公有访问权限的注解名一致。
- 不包含任何成员变量的注解称为标记注解
例如上面声明的 Test11 注解以及基本注解中的 @Override 注解都属于标记注解。根据需要,注解中可以定义成员变量,成员变量以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。代码如下所示:
public @interface MyTag {
// 定义带两个成员变量的注解
// 注解中的成员变量以方法的形式来定义
String name();
int age();
}
如果在注解里定义了成员变量,那么使用该注解时就应该为它的成员变量指定值,如下代码所示。
public class Test12 {
//使用带成员变量的注解时,需要为成员变量赋值
@MyTag(name = "liming", age=12)
public void info(){
}
}
注解中的成员变量也可以有默认值,可使用 default 关键字。如下代码定义了 @MyTag 注解,该注解里包含了 name 和 age 两个成员变量。
public @interface MyTag {
// 定义了两个成员变量的注解
// 使用default为两个成员变量指定初始值
String name() default "李媛";
int age() default 34;
}
如果为注解的成员变量指定了默认值,那么使用该注解时就可以不为这些成员变量赋值,而是直接使用默认值。
public class Test12 {
// 使用带成员变量的注解
// MyTag注释的成员变量有默认值,所以可以不为它的成员变量赋值
@MyTag
public void info(){
}
}
当然也可以在使用 MyTag 注解时为成员变量指定值,如果为 MyTag 的成员变量指定了值,则默认值不会起作用。
根据注解是否包含成员变量,可以分为如下两类。
- 标记注解:没有定义成员变量的注解类型被称为标记注解。这种注解仅利用自身的存在与否来提供信息,如前面介绍的 @Override、@Test 等都是标记注解。
- 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。
现在只学习注解的大致技能点,具体怎么应用 后面慢慢学习。
四、Java元注解作用及使用
元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。Java 5 定义了 4 个注解,分别是 @Documented、@Target、@Retention 和 @Inherited。Java 8 又增加了 @Repeatable 和 @Native 两个注解。这些注解都可以在 java.lang.annotation 包中找到。下面主要介绍每个元注解的作用及使用。
4.1.@Documented
@Documented 是一个标记注解,没有成员变量。用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。
@Documented
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyDocumented {
public String value() default "这是@Documented注解";
}
测试类代码:
@MyDocumented
public class DocumentedTest {
/**
* 测试注解
* @return
*/
@MyDocumented
public String Test(){
return "这是Java教程";
}
}
打开 Java 文件所在的目录,分别输入如下两条命令行:
javac MyDocumented.java DocumentedTest.java
javadoc -d doc MyDocumented.java DocumentedTest.java
如下图:
4.2.@Target
@Target 注解用来指定一个注解的使用范围,即被 @Target 修饰的注解可以用在什么地方。@Target 注解有一个成员变量(value)用来设置适用目标,value 是 java.lang.annotation.ElementType 枚举类型的数组,下表为 ElementType 常用的枚举常量。
名称 | 说明 |
CONSTRUCTOR | 用于构造方法 |
FIELD | 用于成员变量(包括枚举常量) |
LOCAL_VARIABLE | 用于局部变量 |
METHOD | 用于方法 |
PACKAGE | 用于包 |
PARAMETER | 用于类型参数(JDK 1.8新增) |
TYPE | 用于类、接口(包括注解类型)或 enum 声明 |
自定义一个 MyTarget 注解,使用范围为方法如下:
@Target({ElementType.METHOD})
public @interface MyTarget {
}
class Test13{
@MyTarget
String name;
}
会编译错误,错误信息为:
'@MyTarget' not applicable to field
提示此位置不允许使用注解 @MyDocumented,@MyTarget 不能修饰成员变量,只能修饰方法。
4.3.@Retention
@Retention 用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示。
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在 class 文件中有效(即 class 保留)
- RUNTIME:在运行时有效(即运行时保留)
生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
4.4.@Inherited
@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
@Target({ElementType.TYPE})
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}
@MyInherited
class Test14{
public static void main(String[] args) {
System.out.println(Test14.class.getAnnotation(MyInherited.class));
System.out.println(Test14.class.getAnnotation(MyInherited.class));
System.out.println(Test14.class.getAnnotation(MyInherited.class));
}
}
class testA extends Test14{
}
class testB extends Test14{
}
运行结果为:
@com.assemble.MyInherited()
@com.assemble.MyInherited()
@com.assemble.MyInherited()
4.5.@Repeatable
@Repeatable 注解是 Java 8 新增加的,允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。案例如下:
在生活中一个人往往是具有多种身份,例如我是一家公司的老板,同时我还是我妻子的丈夫,更是我父母的孩子,如果希望借助注解的方式来表达该如何呢?
- 首先定义一个Persons类来表示我所有的身份:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
Person[] value();
}
这里@Target是声明Persons注解的作用范围,参数ElementType.Type代表可以给一个类型进行注解,比如类,接口,枚举。@Retention是注解的有效时间,RetentionPolicy.RUNTIME是指程序运行的时候。
- 接下来我们就定义一个注解,这里用到了@Repeatable注解,来真正表达我们的身份:
@Repeatable(Persons.class)
public @interface Person{
String role() default "";
}
@Repeatable括号内的就相当于用来保存该注解内容的容器。
- 然后,为“我”来创建一个实体类:
@Person(role="CEO")
@Person(role="husband")
@Person(role="father")
@Person(role="son")
public class Man{
String name="";
public static void main(String[] args) {
//Java中的getAnnotations()方法可以获取一个类或者方法等程序元素上所有的注解,同时也可以获取注解上的元数据信息。
//该方法返回的是一个Annotation[]数组,它包含了指定元素上所有的注解对象,但是并不包括继承的注解。
Annotation[] annotations = Man.class.getAnnotations();
System.out.println(annotations.length);//输出长度 1
Persons p1=(Persons) annotations[0];
for(Person t:p1.value()){
System.out.println(t.role());
}
}
}
- 最后测试一下,获取所有的身份信息并输出:
@Person(role="CEO")
@Person(role="husband")
@Person(role="father")
@Person(role="son")
public class Man{
String name="";
public static void main(String[] args) {
//Java中的getAnnotations()方法可以获取一个类或者方法等程序元素上所有的注解,同时也可以获取注解上的元数据信息。
//该方法返回的是一个Annotation[]数组,它包含了指定元素上所有的注解对象,但是并不包括继承的注解。
Annotation[] annotations = Man.class.getAnnotations();
System.out.println(annotations.length);//输出长度 1
Persons p1=(Persons) annotations[0];
for(Person t:p1.value()){
System.out.println(t.role());
}
}
}
4.6.@Native
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可