Java Annotation ( 注解 )
JDK 5 开始, Java 增加了对元数据 (MetaData) 的支持,也就是 Annotation (注解) 。Annotation 其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或部署。
Annotation 提供了一种为程序元素设置元数据的方法,从某些方面来看, Annotation 就像修饰符一样,可用于修饰包、接口、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在 Annotation 的 "name=value" 对中。
Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation 对象,然后通过 Annotation 对象来取得注解里的元数据。
Annotation 不影响程序代码的执行,无论增加、删除 Annotation ,代码都始终如一地执行。如果希望让程序中的 Annotation 在运行时起一定作用,只有通过某种配套的工具对 Annotation 中的信息进行访问和处理。
访问和处理 Annotation 的工具统称 APT (Annotation Processing Tool)
1 基本 Annotation
---------------------------------------------------------------------------------------------------
Annotation 必须使用工具来处理,工具负责提取 Annotation 里包含的元数据,工具还会根据这些元数据增加额外的功能。
使用 Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素。
5 个基本的 Annotation:
@Overrid
@Deprecated
@SuppressWarnings
@SafeVarargs : Java 7 新增的
@FunctionalInterface :Java 8 新增的
限定重写父类方法:@Overrid
---------------------------
@Overrid 就是用来指定方法覆盖的,它可以强制一个子类必须覆盖父类的方法。
例子:
public class Fruit
{
public void info()
{
System.out.println("水果的info方法...");
}
}
class Apple extends Fruit
{
// 使用@Override指定下面方法必须重写父类方法
@Override
public void inf0()
{
System.out.println("苹果重写水果的info方法...");
}
}
@Overrid 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。
@Overrid 只能修饰方法,不能修饰其他程序元素。
标记已过时:@Deprecated
------------------------
@Deprecated 用于标识某个程序元素(类、方法)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。
例子:
class Apple
{
// 定义info方法已过时
@Deprecated
public void info()
{
System.out.println("Apple的info方法");
}
}
public class DeprecatedTest
{
public static void main(String[] args)
{
// 下面使用info方法时将会被编译器警告
new Apple().info();
}
}
@Deprecated 的作用与文档注释中的 @deprecated 标记的作用基本相同,但它们的用法不同,前者是 JDK 5 才支持的注解,无须放在文档注释语法 (/** ... */部分 )中,而是直接用于修饰程序中的程序单元,如方法、类、接口等。
抑制编译器警告: @SuppressWarnings
-----------------------------------
@SuppressWarnings 指示被该 Annotation 修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。
@SuppressWarnings 会一直作用于该程序元素的所有子元素。例如,使用 @SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消另一个编译器警告,那么该方法将会同时取消显示这两个编一起去警告。
通常情况下,如果程序中使用没有泛型限制的集合会引起编译器警告,为了避免这种编译器警告,可以使用 @SuppressWarnings 修饰。
例子:
// 关闭整个类里的编译器警告
@SuppressWarnings(value="unchecked")
public class SuppressWarningsTest
{
public static void main(String[] args)
{
List<String> myList = new ArrayList(); // ①
}
}
当使用 @SuppressWarnings Annotation 来关闭编译器警告时,一定要在括号里使用 name=value 的形式为该 Annotation 的成员变量设置值。
Java 7 的“堆污染”警告与 @SafeVarargs
--------------------------------------
如下代码可能导致运行时异常
List list = new ArrayList<Integer>();
list.Add(20); //添加元素时引发 unchecked 异常
// 下面的代码引起 “未经检查的转换” 的警告。编译、运行时完全正常
List<String> ls = list; ①
// 但只要访问ls 里的元素,如下面代码就会引起运行时异常
System.out.println(ls.get(0));
Java 把引发这种错误的原因称为“堆污染” (Heap pollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”,如 ① 代码所示。
对于形参数可变的方法,该形参的类型又是泛型,这将更容易导致“堆污染”,例如:
public class ErrorUtils
{
@SafeVarargs
public static void faultyMethod(List<String>... listStrArray)
{
// Java语言不允许创建泛型数组,因此listArray只能被当成List[]处理
// 此时相当于把List<String>赋给了List,已经发生了“擦除”
List[] listArray = listStrArray;
List<Integer> myList = new ArrayList<Integer>();
myList.add(new Random().nextInt(100));
// 把listArray的第一个元素赋为myList
listArray[0] = myList;
String s = listStrArray[0].get(0);
}
}
由于该方法有个形参 List<String>...类型,个数可变的形参相当于数组,但Java又不支持泛型数组,因此程序只能把 List<String>...当成 List[]处理,这里就发生了“堆污染”。
Java 7 会在定义该方法时就发出“堆污染”警告,这样保证开发者更早地注意到程序可能存在的漏洞。
但在有些时候,开发者不希望看到这个警告,则可以用如下三种方式来抑制警告:
1.使用 @SafeVarargs 修饰引发该警告的方法或构造器
2.使用 @SuppressWarnings("unchecked")修饰。
3.编译时使用 -Xlint:varargs 选项
弟三种方式比较少用,通常可以选择第一种或第二种方式,尤其是使用 @SafeVarargs 修饰引发警告的方法或构造器。
Java 8 的函数式接口与 @FunctionalInterface
-------------------------------------------
Java 8 规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),该接口就是函数式接口。
@FunctionalInterface 就是用来指定某个接口必须是函数式接口。
@FunctionalInterface
public interface FunInterface
{
static void foo()
{
System.out.println("foo类方法");
}
default void bar()
{
System.out.println("bar默认方法");
}
void test(); // 只定义一个抽象方法
// void abc();
}
编译上面的程序,可能丝毫看不出程序中 @FunctionalInterface 有任何作用, @FunctionalInterface 只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错。
@FunctionalInterface 主要是帮助程序员避免一些低级错误,例如,在 FunInterface 接口中再增加一个抽象方法 abc(),编译程序时将出现错误。
@FunctionalInterface 只能修饰接口,不能修饰其他程序元素。
*
*
*
2 JDK 的元 Annotation
----------------------------------------------------------------------------------------
java.lang.annotation 下的6个 Meta Annotation (元 Annotation)
使用 @Retention
----------------------------
@Retention 只能用于修饰 Annotation 定义,用于指定被修饰的 Annotation 可以保留多长时间,@Retention 包含一个 RetentionPolicy 类型的 value 成员变量,所以使用@Retention 时必须为该 value 成员变量指定值。
value 成员变量的值只能是如下三个:
RetentionPolicy.CLASS: 编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时, JVM 不可获取 Annotation 信息,这是默认值。
RetentionPolicy.RUNTIME:编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时, JVM 也可获取 Annotation 信息,程序可以通过反射获取该 Annotation 信息。
RetentionPolicy.SOURCE: Annotation 只保留在源代码中,编译器直接丢弃这种 Annotation。
// 定义下面的 Testable Annotation 保留到运行时
@Retention(value=RetentionPolicy.RUNTIME)
public @interface Testable{}
//定义下面的 Testable Annotation 将被编译器直接丢弃
@Retention(RetentionPolicy.SOURCE)
public @interface Testable{}
上面代码中使用 @Retention 元 Annotation 时,并未通过 value=RetentionPolicy.SOURCE 的方式来为该成员变量指定值,这是因为但 Annotation 的成员变量名为 value 时,程序中可以直接在 Annotation 后的括号里指定该成员变量的值,无须指定 name=value 的形式。
如果使用注解时只需要为 value 成员变量指定值,则使用该注解时可以直接在该注解后的括号里指定 value 成员变量的值,无须使用 "name=变量值" 的形式。
使用 @Target
------------------
@Target 也只能修饰一个 Annotation 定义,它用于指定被修饰的 Annotation 能用于修饰哪些程序单元。
@Target 元 Annotation 也包含一个名为 value 的成员变量,该成员变量的值只能是如下几个:
ElementType.ANNOTATION_TYPE: 指定该策略的 Annotation 只能修饰 Annotation
ElementType.CONSTRUCTOR: 指定该策略的 Annotation 只能修饰构造器
ElementType.FIELD: 指定该策略的 Annotation 只能修饰成员变量
ElementType.LOCAL_VARIABLE: 指定该策略的 Annotation 只能修饰局部变量
ElementType.METHOD: 指定该策略的 Annotation 只能修饰方法
ElementType.PACKAGE: 指定该策略的 Annotation 只能修饰 包定义
ElementType.PARAMETER: 指定该策略的 Annotation 可以修饰参数
ElementType.TYPE: 指定该策略的 Annotation 可以修饰类、接口 (包括注解类型)或枚举定义
例子:
//@ActionListenerFor Annotation 只能修饰成员变量
@Target(ElementType.FIELD)
public @interface ActionListenerFor{}
// @Testable Annotation 只能修饰方法
@Target(ElementType.METHOD)
public @interface Testable{}
使用 @Documented
----------------------------------------
@Documented 用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档
如果定义 Annotation 类时使用了 @Documented 修饰,则所有使用该 Annotation 修饰的程序元素的 API 文档中将会包含该 Annotation 的说明
下面定义了一个 Tesble Annotation, 程序使用 @Documented 来修饰 @Testable Annotation 定义,所以该 Annotation 将被 javadoc 工具提取。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 定义Testable Annotation将被javadoc工具提取
@Documented
public @interface Testable
{
}
上面代码中指定了 javadoc 工具生成的 API 文档将提取 @Testable 的使用信息。
下面代码定义了一个 MyTest 类,该类中的 info() 方法使用了 @Testable 修饰
public class MyTest
{
// 使用@Test修饰info方法
@Testable
public void info()
{
System.out.println("info方法...");
}
}
使用 @Inherited
--------------------------------------
@Inherited 元 Annotation 指定被它修饰的 Annotation 将具有继承性-----如果某个类使用了 @Xxx注解修饰(定义该 Annotation 时使用了 @inherited 修饰),则其子类将自动被 @Xxx 修饰
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Inheritable
{
}
上面代码表明 @Inheritable 具有继承性, 如果某个类使用了 @Inheritable 修饰,则该类的子类将自动使用 @Inheritable 修饰
// 使用@Inheritable修饰的Base类
@Inheritable
class Base
{
}
// TestInheritable类只是继承了Base类,
// 并未直接使用@Inheritable Annotiation修饰
public class InheritableTest extends Base
{
public static void main(String[] args)
{
// 打印TestInheritable类是否具有@Inheritable修饰
System.out.println(InheritableTest.class
.isAnnotationPresent(Inheritable.class)); //输出: true
}
}
*
*
*
3 自定义 Annotation
---------------------------------------------------------------------------------------------
定义新的 Annotation 类型使用 @interface 关键字 (在原有 interface 关键字前增加 @ 符号)
例子:
//定义一个简单的 Annotation 类型
public @interface Test
{
}
定义了新的 Annotation 之后,就可以在程序的任何地方使用该 Annotation, 通常会把 Annotation 放在所有修饰符之前,而且由于使用 Annotation 时可能还需要为成员变量指定值,因而 Annotation 的长度可能较长,所以通常把 Annotation 单独另放一行。
//使用 @Test 修饰类定义
@Test
public class MyClass
{
...
}
默认情况下, Annotation 可以用于修饰任何程序元素。
Annotation 不仅可以是这种简单的 Annotation, 还可以带成员变量, Annotation 的成员变量在 Annotation 定义中以无形参的方法形式声明,其方法名和返回值定义了该成员变量的名称和类型
例子:
public @interface MyTag
{
// 定义带两个成员变量的 Annotation
// Annotation 中的成员变量以方法的形式定义
String name();
int age();
}
上面定义 Annotation 的代码与定义接口的语法非常类似,只是使用 @interface 关键字来定义,而接口使用 interface 定义。
一旦在 Annotation 里定义了成员变量之后,使用该 Annotation 时就应该为该 Annotation 的成员变量指定值
public class Test
{
//使用带成员变量的 Annotation 时,需要为成员变量赋值
@MyTag(name="xx", age=6)
public void info()
{
}
}
也可以在定义 Annotation 的成员变量时为其指定初始值(默认值),指定成员变量的初始值使用 default 关键字。
public @interface MyTag
{
//定义了两个成员变量的 Annotation
//使用 default 为两个成员变量指定初始值
String name() default "yeeku" ;
int age() default 32;
}
如果为 Annotation 的成员变量指定了默认值,使用该 Annotation 时则可以不为这些成员变量指定值,而是直接使用默认值
例子:
public class Test
{
//使用带成员变量的 Annotation, 因为它的成员变量有默认值,所以可以不为它的成员变量指定值
@MyTag
public void info()
{
}
}
当然也可以在使用 MyTag Annotation 时为成员变量指定值,默认值不会起作用。
根据 Annotation 是否可以包含成员变量,可以把 Annotation 分为如下两类:
标记 Annotation: 没有定义成员变量的 Annotation 类型被称为标记。这种 Annotation 仅利用自身的存在与否来提供信息,如 @Overrid, @Test 等 Annotation
元数据 Annotation: 包含成员变量的 Annotation, 因为它们可以接受更多的元数据,所以被称为元数据 Annotation
提取 Annotation 信息
------------------------------------------------
使用 Annotation 修饰了类、方法、成员变量等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的工具来提取并处理 Annotation 信息
Java 使用 java.lang.annotation.Annotation 接口来代表程序元素前面的注解,该接口是所有注解的父接口。
Java 5 在 java.lang.reflect 包下新增 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素,有如下几个实现类:
Class: 类定义
Constructor: 构造器定义
Field: 类的成员变量定义
Method: 类的方法定义
Package: 包定义
java.lang.reflect 包所提供的反射 API 增加了读取运行时 Annotation 的能力。
只有在定义 Annotation 是使用了 @Retention(RetentionPolicy.RUNTIME) 修饰,该 Annotation 才会在运行时可见, JVM 才会在装载 *.class 文件时读取保存在 class 文件中的 Annotation.
AnnotatedElement接口是所有程序元素的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象 (如 Class, Method, Constructor 等)之后,程序就可以调用该对象的 AnnotatedElement 接口方法来访问 Annotation 信息。
// 获取 Test 类的 info方法的所有注解
Annotation[] aArray = Class.forName("Test").getMethod("info",null).getAnnotations();
// 遍历所有注解
for( Annotation an : aArray)
{
System.out.println(an);
}
如果需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。
例子:
//获取 tt 对象的 info 方法所包含的所有注解
Annotation[] annotation = Class.forName("Test").getMethod("info",null).getAnnotations();
//遍历每个注解对象
for (Annotation tag : annotation)
{
//如果 tag 注解是 MyTag1 类型
if ( tag instanceof MyTag1)
{
System.out.println("Tag is: " + tag);
//将 tag 强制类型转换为 MyTag1
//输出 tag 对象的name 和 age 两个成员变量的值
System.out.println("tag.name(): " + ((MyTag1)tag).name());
System.out.println("tag.age(): " + ((MyTag1)tag).age());
}
//如果 tag 注解是 MyTag2 类型
if ( tag instanceof MyTag2)
{
System.out.println("Tag is: " + tag);
//将 tag 强制类型转换为 MyTag2
//输出 tag 对象的name 和 age 两个成员变量的值
System.out.println("tag.name(): " + ((MyTag2)tag).name());
System.out.println("tag.age(): " + ((MyTag2)tag).age());
}
}
Java 8 新增的重复注解
---------------------------------------------------------------------------
Java 8 之前,同一个程序元素前最多只能使用一个相同类型的 Annotation; 如果需要在同一个元素前使用相同类型的 Annotation, 则必须使用 Annotation “容器”
Java 8 之前只能这样写:
@Results({@Result(name="failure", location="failed.jsp"),
@Result(name="success", location="succ/jsp")})
public Action FooAction{...}
实质是,@Results注解只包含一个名字为 value, 类型为 Result[] 的成员变量,程序指定的多个 @Result将作为 @Results 的value属性(数组类型)的数组元素。
Java 8 开始,传统的语法得到简化。
开发重复注解需要使用 @Repeatable 修饰
// 指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag
{
// 为该注解定义2个成员变量
String name() default "疯狂软件";
int age();
}
使用 @Repeatable 时必须为value成员变量指定值,该成员变量的值应该是一个“容器”注解----该“容器”注解可以包含多个 @FkTag, 因此还需要定义如下的"容器"注解
// 指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTags
{
// 定义value成员变量,该成员变量可接受多个@FkTag注解
FkTag[] value();
}
注意:“容器”注解的保留期必须和它所包含的注解的保留期相同或更长,否则编译器报错
@FkTag(age=5)
@FkTag(name="疯狂Java" , age=9)
//@FkTags({@FkTag(age=5),
// @FkTag(name="疯狂Java" , age=9)})
public class FkTagTest
{
public static void main(String[] args)
{
Class<FkTagTest> clazz = FkTagTest.class;
/* 使用Java 8新增的getDeclaredAnnotationsByType()方法获取
修饰FkTagTest类的多个@FkTag注解 */
FkTag[] tags = clazz.getDeclaredAnnotationsByType(FkTag.class);
// 遍历修饰FkTagTest类的多个@FkTag注解
for(FkTag tag : tags)
{
System.out.println(tag.name() + "-->" + tag.age());
}
/* 使用传统的getDeclaredAnnotation()方法获取
修饰FkTagTest类的@FkTags注解 */
FkTags container = clazz.getDeclaredAnnotation(FkTags.class);
System.out.println(container);
}
}
重复注解只是一种简化写法,这种简化写法是一种假象:多个重复注解其实会被作为“容器”注解的value成员变量的数组元素
Java 8 新增的 Type Annotation
------------------------------------------
Java 8 为 ElementType 枚举增加了 TYPE_PARAMTER, TYPE_USE 两个枚举值,这样就允许定义枚举时使用 @Target(ElementType.TYPE_USE) 修饰,这种注解被称为 Type Annotation (类型注解)
Type Annotation 可用在任何用到类型的地方