从JDK 5开始,Java增加了对元数据(MetaData)的支持,也就是Annotation(注释)。Annotation提供了一种为程序元素设置元数据的方法,从某些方面来看,Annotation就想修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在Annotation的”name = value”对中
Annotation是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注释里的元数据
Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据,且不会影响程序代码的执行,无论增加、删除Annotation,代码都始终如一地执行。如果希望让程序中的Annotation在运行时起一定的作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,访问和处理Annotation的工具统称为APT(Annotation Processing Tool)
基本Annotation
使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素
5个基本的Annotation
@SuppressWarnings
@SafeVarargs
@FunctionalInterface
限定重写父类方法:@Override
@Override 就是用来指定方法覆载,它可以强制一个子类必须覆盖父类的方法。@Override的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。@Override主要是帮助程序员避免一些低级错误,如重写info()方法,却手误写成了inf(),编译器不会报错,你可能找半天才能找到错误
@Override 只能修饰方法,不能修饰其他程序元素
@Deprecated 用于表示某个程序元素(类,方法等)已过时,当其他程序使用已过时的类,方法时,编译器将会给出警告
抑制编译器警告:@SuppressWarnings
@SuppressWarnings 指示被该Annotation修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。@SuppressWarnings 会一直作用域该程序元素的所有子元素,例如,使用@SuppressWarnings修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告
Java 7的“堆污染”警告与@SafeVarargs
List list = new ArrayList();
list.add(10); //添加元素时引发unchecked异常
// 下面代码引起“未经检查的转换”的警告,但编译、运行时完全正常
List temp = list; // ①
// 但只要访问temp里的元素,就会引起运行时异常
System.out.println(temp.get(0));
“堆污染”(Heap pollution):当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会方式这种“堆污染”
Java会在定义该方法时就发出“堆污染”警告,这样保证开发者“更早”地注意到程序中可能存在的“漏洞”。有些时候,开发者不希望看到这个警告,则可以使用如下三种方式来“抑制”这个警告
使用@SafeVarargs 修饰引发该警告的方法或构造器
使用@SuppressWarnings("unchecked")修饰
编译时使用-Xlint:varargs选项(很少使用)
Java 8的函数式接口与@FunctionalInterface
Java 8规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口就是函数式接口。该注解只能够修饰接口,不能够修饰其他程序元素。@FunctionalInterface 只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错
@FunctionalInterface 只能修饰接口,不能修饰其他程序元素
JDK的元Annotation
JDK除了在java.lang下提供了5个基本的Annotation之外,还在java.lang.annotation包下提供了6个Meta Annotation,其中有5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用于定义Java 8新增的重复注解
使用@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{}
使用@Target
@Target 也只能用来修饰一个Annotation定义,它用于指定被修饰的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{}
使用Documented
@Documented 用于指定被该元Annotation修饰的Annotation类将被javadoc工具提取成文档,如果定义Annotation类时候使用了@Documented 修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该Annotation说明
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 定义Param Annotation将被javadoc工具提取
@Documented
public @interface Param
{
long id();
String name();
String team() default "Cleveland";
}
public class Person
{
public static void main(String[]args) { ... }
// 使用@Param修饰toPerson()方法
@Param(id = 23, name = "James")
public void toPerson() { ... }
}
使用@Inherited
@Inherited 元Annotation指定被它修饰的Annotation将具有继承性——如果某个类使用了@Xxx注解(定义该Annotation时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰
自定义Annotation
定义Annotation
定义一个新的Annotation与定义一个接口类似,需要使用@interface关键字,例如下面定义了一个名为Param的Annotation,并在Test类中使用它:
public @interface Param { }
可以在程序的任何地方使用该Annotation,可用于修饰程序中的类、方法、变量、接口等定义。通常会把Annotation放在所有修饰符之前,另放一行
// 使用@Param修饰类定义
@Param
public class Test {
public static void main(String[]args) { }
}
在默认情况下,Annotation可用于修饰任何程序元素,包括类、接口、方法等。如普通方法一样,Annotation还可以带成员变量,Annotation的成员变量在Annotation定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型,如:
public @interface Param
{
long id();
String name();
String team() default "Cleveland";
}
一旦在Annotation里定义了成员变量,使用时就需要为其指定值;也可以为成员变量指定初始值(默认值),指定成员变量的初始值可使用default关键字,此时可以不为这些成员变量指定值
@Param(id = 2, name = "Irving")
public class Animal {
public static void main(String[]args) { ... }
}
根据Annotation按是否包含成员变量,Annotation分为两类:
标记Annotation:没有定义成员变量的Annotation类型称为标记。这种Annotation仅利用自身的存在与否来为我们提供信息,例如@Override 、@Deprecated等
元数据Annotation:包含成员变量的Annotation,因为它们可以接受更多的元数据
提取annotation信息
使用Annotation修饰了类、方法、成员变量等成员后,这些Annotation不会自己生效,必须由开发者提供相应的工具来提取并处理Annotation信息
AnnotatedElement接口是所有程序元素(如Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如Class、Method、Constructor等)之后,程序就可以调用该对象的如下几个方法来访问Annotation信息
T getAnnotation(Class annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null
T getDeclaredAnnotation(Class annotationClass):该方法尝试获取直接修饰该程序元素、指定类型的Annotation。如果该类型的注解不存在,则返回null
Annotation[] getAnnotations():返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组
Annotation[] getDeclaredAnnotations():返回直接修饰该程序元素的所有Annotation
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass) :判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false
T[] getAnnotationsByType(Class annotationClass):该方法的功能与getAnnotation()方法基本相似。使用该方法获取修饰该元素、指定类型的多个Annotation
T[] getDeclaredAnnotationsByType(Class annotationClass):该方法的功能与getDeclaredAnnotation()方法基本相似。使用该方法获取直接修饰该元素、指定类型的多个Annotation
// 获取Test类的info方法里的所有注解,并将这些注解打印出来
Annotation[] aArray = Class.forName("Test").getMethod("info").getAnnotations();
// 遍历所有注解
for (Annotation an : aArray)
{
System.out.println(an);
}
如果需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的主家楼下,然后通过注解对象的抽象方法来访问这些元数据
// 获取tt对象的info方法所包含的所有注解
Annotation[] annotation = tt.getClass.forName().getMethod("info").getAnnotations();
// 遍历每个注解对象
for (Annotation tag : annotation)
{
// 如果tag注解是MyTag1类型
if ( tag instanceof MyTag1)
{
System.out.println("Tag is: " + tag);
// 将tag强制类型转换伟MyTag1
// 输出tag对象的method1和method2两个成员变量的值
System.out.println("tag.name(): " + ((MyTag1)tag).method1());
System.out.println("tag.age(): " + ((MyTag1)tag).method2());
}
// 如果tag注解是MyTag2类型
if ( tag instanceof MyTag2)
{
System.out.println("Tag is: " + tag);
// 将tag强制类型转换伟MyTag2
// 输出tag对象的method1和method2两个成员变量的值
System.out.println("tag.name(): " + ((MyTag2)tag).method1());
System.out.println("tag.age(): " + ((MyTag2)tag).method2());
}
}
使用Annotation的示例
e.g. One
Annotation
Testable没有任何成员变量,仅是一个标记Annotation,作用是标记哪些方法是可测试的。程序通过判断该Annotation存在与否来决定是否运行指定方法
import java.lang.annotation.*;
// 使用JDK的元数据Annotation:Retention
@Retention(RetentionPolicy.RUNTIME)
// 使用JDK的元数据Annotation:Target
@Target(ElementType.METHOD)
// 定义一个标记注解,不包含任何成员变量,即不可传入元数据
public @interface Testable
{
}
@Testable 用于标记哪些方法是可测试的,该Annotation可以作为JUnit测试框架的补充。在JUnit框架中,测试用例的测试方法必须以test开头。如果使用@Testable 注解,则可把任何方法标记为可测试的
public class MyTest
{
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m1()
{
}
public static void m2()
{
}
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m3()
{
throw new IllegalArgumentException("参数出错了!");
}
public static void m4()
{
}
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m5()
{
}
public static void m6()
{
}
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m7()
{
throw new RuntimeException("程序业务出现异常!");
}
public static void m8()
{
}
}
注解处理工具分析目标类,如果目标类中的方法使用了@Testable 注解修饰,则通过反射来运行该测试方法
import java.lang.reflect.*;
public class ProcessorTest
{
public static void process(String clazz)
throws ClassNotFoundException
{
int passed = 0;
int failed = 0;
// 遍历clazz对应的类里的所有方法
for (Method m : Class.forName(clazz).getMethods())
{
// 如果该方法使用了@Testable修饰
if (m.isAnnotationPresent(Testable.class))
{
try
{
// 调用m方法
m.invoke(null);
// 测试成功,passed计数器加1
passed++;
}
catch (Exception ex)
{
System.out.println("方法" + m + "运行失败,异常:"
+ ex.getCause());
// 测试出现异常,failed计数器加1
failed++;
}
}
}
// 统计测试结果
System.out.println("共运行了:" + (passed + failed)
+ "个方法,其中:\n" + "失败了:" + failed + "个,\n"
+ "成功了:" + passed + "个!");
}
}
public class RunTests
{
public static void main(String[] args)
throws Exception
{
// 处理MyTest类
ProcessorTest.process("MyTest");
}
}
e.g. Two
通过使用Annotation来简化事件编程,在传统的事件编程中总是需要通过addActionListener()方法来为事件源绑定事件监听器,下面的示例通过@ActionListenerFor来为程序中的按钮绑定事件监听器
import java.lang.annotation.*;
import java.awt.event.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor
{
// 定义一个成员变量,用于设置元数据
// 该listener成员变量用于保存监听器实现类
Class extends ActionListener> listener();
}
使用@ActionListenerFor 注解来为两个按钮绑定事件监听器
import java.awt.event.*;
import javax.swing.*;
public class AnnotationTest
{
private JFrame mainWin = new JFrame("使用注解绑定事件监听器");
// 使用Annotation为ok按钮绑定事件监听器
@ActionListenerFor(listener=OkListener.class)
private JButton ok = new JButton("确定");
// 使用Annotation为cancel按钮绑定事件监听器
@ActionListenerFor(listener=CancelListener.class)
private JButton cancel = new JButton("取消");
public void init()
{
// 初始化界面的方法
JPanel jp = new JPanel();
jp.add(ok);
jp.add(cancel);
mainWin.add(jp);
ActionListenerInstaller.processAnnotations(this); // ①
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}
public static void main(String[] args)
{
new AnnotationTest().init();
}
}
// 定义ok按钮的事件监听器实现类
class OkListener implements ActionListener
{
public void actionPerformed(ActionEvent evt)
{
JOptionPane.showMessageDialog(null , "单击了确认按钮");
}
}
// 定义cancel按钮的事件监听器实现类
class CancelListener implements ActionListener
{
public void actionPerformed(ActionEvent evt)
{
JOptionPane.showMessageDialog(null , "单击了取消按钮");
}
}
定义了两个JButton按钮,并使用@ActionListenerFor 注解为这两个按钮绑定了事件监听器,使用@ActionListenerFor 注解时传入了listener元数据,该数据用于设定每个按钮的监听器实现类。程序①处代码使用ActionListenerInstaller类来处理本程序中的注解,该处理器分析目标对象中的所有成员变量,如果该成员变量签使用了@ActionListenerFor修饰,则取出该Annotation中的listener元数据,并根据该数据来绑定事件监听器
import java.lang.reflect.*;
import java.awt.event.*;
import javax.swing.*;
public class ActionListenerInstaller
{
// 处理Annotation的方法,其中obj是包含Annotation的对象
public static void processAnnotations(Object obj)
{
try
{
// 获取obj对象的类
Class cl = obj.getClass();
// 获取指定obj对象的所有成员变量,并遍历每个成员变量
for (Field f : cl.getDeclaredFields())
{
// 将该成员变量设置成可自由访问。
f.setAccessible(true);
// 获取该成员变量上ActionListenerFor类型的Annotation
ActionListenerFor a = f.getAnnotation(ActionListenerFor.class);
// 获取成员变量f的值
Object fObj = f.get(obj);
// 如果f是AbstractButton的实例,且a不为null
if (a != null && fObj != null
&& fObj instanceof AbstractButton)
{
// 获取a注解里的listner元数据(它是一个监听器类)
Class extends ActionListener> listenerClazz = a.listener();
// 使用反射来创建listner类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton)fObj;
// 为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
根据@ActionListenerFor注解的元数据取得了监听器实现类,然后通过反射来创建监听器对象,接下来将监听器对象绑定到指定的按钮(按钮由被@ActionListenerFor修饰的Field表示)
Java8新增的重复注解
Java8允许使用多个相同类型的Annotation来修饰同一个类
@Result (name = "failure", location = "failed.jsp")
@Result (name = "success", location = "succ.jsp")
public Acton FooAction{...}
如果定义了@FkTag(无@Repeatable版)注解,该注解包括两个成员变量。但该注解默认不能作为重复注解使用,如果使用两个以上的如下注解修饰同一个类,编译器会报错
开发重复注解需要使用@Repeatable 修饰。使用@Repeatable修饰该注解,使用@Repeatable时必须为value成员变量指定值,该成员变量的值应该是一个“容器”注解——该容器注解可以包含多个@FkTag
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag
{
// 为该注解定义2个成员变量
String name() default "NBA球员";
int number();
}
“容器”注解可包含多个@FkTag,因此需要定义如下的“容器”注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // ①
@Target(ElementType.TYPE)
public @interface FkTags
{
// 定义value成员变量,该成员变量可接受多个@FkTag注解
FkTag[] value(); // ②
}
代码①指定了@FkTags 注解信息可保留到运行时,这是必需的,因为@FkTag 注解信息需要保留到运行时,如果@FkTags 注解只能保留到源代码级别或类文件,将会导致@FkTags 的保留期小于@FkTag 的保留期,如果程序将多个@FkTag注解放入@FkTags中,若JVM丢弃了@FkTags注解,自然也就丢弃了@FkTag的信息
代码②定义了一个FkTag[]类型的value成员变量,这意味着@FkTags 注解的value成员变量可接受多个@FkTags 注解可作为@FkTag 的容器
“容器”注解的保留期必须必它所包含的注解的保留期更长,否则编译器会报错
传统代码使用该注解
@FkTags({@FkTag(number = 23), @FkTag(name = "Westbrooks", number = 0)})
由于@FkTags是重复注解,因此可直接使用两个@FkTag注解,系统依然将两个@FkTag注解作为@FkTags的values成员变量的数组元素
@FkTag(number = 23)
@FkTag(name = "Westbrooks", number = 0)
重复注解是一种简化写法,这种简化写法是一种假象:多个重复注解会被作为“容器”注解的value成员变量的数组元素
@FkTag(number = 23)
@FkTag(name = "Westbrooks", number = 0)
public class FkTagTest
{
public static void main(String[] args)
{
Class 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());
}
}
}
运行结果:
NBA球员-->23
Westbrooks-->0
@FkTags(value=[@FkTag(name=NBA球员, age=23), @FkTag(name=Westbrooks, age=0)])
Java8新增的Type Annotation
Java8为ElementType枚举增加了TYPE_PARAMETER、TYPE_USE两个枚举值,允许定义枚举时使用@Target(ElementType.TYPE_USE)修饰,这种注解称为Type Annotation(类型注解),Type Annotation可用在任何用到类型的地方
允许在如下位置使用Type Annotation
创建对象(用new关键字创建)
类型转换
使用implements实现接口
使用throws声明抛出异常
import java.util.*;
import java.io.*;
import javax.swing.*;
import java.lang.annotation.*;
// 定义一个简单的Type Annotation,不带任何成员变量
@Target(ElementType.TYPE_USE)
@interface NotNull{}
// 定义类时使用Type Annotation
@NotNull
public class TypeAnnotationTest
implements @NotNull /* implements时使用Type Annotation */ Serializable
{
// 方法形参中使用Type Annotation
public static void main(@NotNull String[] args)
// throws时使用Type Annotation
throws @NotNull FileNotFoundException
{
Object obj = "fkjava.org";
// 强制类型转换时使用Type Annotation
String str = (@NotNull String)obj;
// 创建对象时使用Type Annotation
Object win = new @NotNull JFrame("俄克拉荷马雷霆");
}
// 泛型中使用Type Annotation
public void foo(List info){}
}