从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

限定重写父类方法:@Override

@Override 就是用来指定方法覆载,它可以强制一个子类必须覆盖父类的方法。@Override的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。@Override主要是帮助程序员避免一些低级错误,如重写info()方法,却手误写成了inf(),编译器不会报错,你可能找半天才能找到错误

@Override 只能修饰方法,不能修饰其他程序元素

标示已过时:@Deprecated

@Deprecated 用于表示某个程序元素(类,方法等)已过时,当其他程序使用已过时的类,方法时,编译器将会给出警告

抑制编译器警告:@SuppressWarnings

@SuppressWarnings 指示被该Annotation修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。@SuppressWarnings 会一直作用域该程序元素的所有子元素,例如,使用@SuppressWarnings修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告

Java 7的“堆污染”警告与@SafeVarargs

List list = new ArrayList<Integer>();
list.add(10); //添加元素时引发unchecked异常
// 下面代码引起“未经检查的转换”的警告,但编译、运行时完全正常
List<String> 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 extends Annotation> T getAnnotation(Class<T> annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null

  • <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass):该方法尝试获取直接修饰该程序元素、指定类型的Annotation。如果该类型的注解不存在,则返回null

  • Annotation[] getAnnotations():返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组

  • Annotation[] getDeclaredAnnotations():返回直接修饰该程序元素的所有Annotation

  • boolean isAnnotationPresent(Class<?extends Annotation> annotationClass) :判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false

  • <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass):该方法的功能与getAnnotation()方法基本相似。使用该方法获取修饰该元素、指定类型的多个Annotation

  • <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> 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<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());
        }
    }
}

运行结果:

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<@NotNull String> info){}
}

布still
461 声望32 粉丝

数据挖掘、用户行为研究、用户画像