Java 注解的使用

Annotation的分类

注解为JDK1.5引入的新内容,调用形式为@Annotation
注解的本质是接口。
注解不影响Java代码的执行。但在运行时,可以通过一些手段例如反射,获取注解的信息,并对其进行处理。

Annotation分为如下3类:

  1. JDK系统注解
  2. 元注解
  3. 自定义注解

JDK系统注解

@Override

@Override注解只能使用在方法上。它用来标识出该方法是用来重写或实现父类或者接口方法的。例如:

class A {
    public void fun1() {}
}

class B extends A {
    @Override // 重写A的fun1方法
    public void fun1() {}
}

interface I {
    void fun2();
}

class C implements I {
    @Override // 实现接口I的fun2方法
    public void fun2() {}
}

下面问题来了,大家会发现,如果删除上面例子中的@Override注解,代码并不会有任何错误。那么要这个注解有什么用呢?
有这么一个例子,我们写了一个Student类,需要重写它的toString方法:

class Student {
    private String name;
    private int age;
    // 省略setter和getter

    public String tostring() { //这里大家发现问题了吗?
        return "name: " + this.name + " age: " + this.age;
    }
}

很不幸的是这里程序员粗心的把toString写成了tostring。但是IDE不会有任何错误提示。IDE认为我们的意图是定义一个新方法tostring,而不是去重写方法toString。
我们尝试在错误的tostring方法上加入override注解:

@Override
public String tostring() {
    return "name: " + this.name + " age: " + this.age;
}

编辑器会有如下错误提示:


Screen Shot 2018-07-21 at 10.27.33 am.png

到这里大家一定都意识到@Override注解的重要性了吧。
这里总结一下,如果定义方法的目的就是为了实现或重写其他方法,务必要加上@Override注解。

@Deprecated

如果一个方法有了更好的替代品,为了兼容性暂时保留但是不建议其他人继续使用需要怎么办?这时候@Deprecated注解派上用场了。调用被@Deprecated修饰的方法会得到编辑器的警告,同时会被标记为删除线:


Screen Shot 2018-07-21 at 10.42.10 am.png

@SuppressWarnnings

编译器很聪明会自动检查代码中的问题给予我们警告。接着上面的例子,如果我们确实需要调用一个被标记为deprecated的方法,又不想忍受编辑器的警告,难道就没有办法了吗?@SupressWarnings可以帮我们这个忙。


Screen Shot 2018-07-21 at 10.48.39 am.png

我们发现加入了@SuppressWarnings注解后,编译器的告警消失,并且对deprecated方法的调用也不会被标记上删除线。

@FunctionalInterface

该注解为Java 8 之后新增加的注解,目的是为了配合新增加的lambda表达式使用。具体Lambda表达式如何使用在这里暂不介绍,请关注本人其他的博客。

Java 8 新增加的Lambda表达式体现了函数式编程的思想,本质上仍然是一个匿名内部类,但是该匿名内部类中只能有一个未实现的方法。
为了约束接口中的抽象方法数量,引入了@FunctionalInterface接口
其作用为被该注解修饰的接口,里面的抽象方法有且只能有一个。如果不符合条件会给出警告。

public class Demo {
    public static void main(String[] args) {
        MyList myList = new MyList<>();

        myList.addItems(Arrays.asList("abc", "def", "ghi"));

        // 使用Lambda表达式
        myList.myForEach(s -> {
            String uppercaseString = s.toUpperCase();
            System.out.println(uppercaseString);
        });
    }
}

class MyList {
    private List list = new ArrayList<>();

    public void addItems(List itemList) {
        list.addAll(itemList);
    }

    // 传入实现了MyForEachFunction接口的对象。使用该方法可以传入lambda表达式
    public void myForEach(MyForEachFunction fun) { 
        for (T t : list) {
            fun.doForEach(t);
        }
    }
}

@FunctionalInterface
interface MyForEachFunction {
    void doForEach(T t); // 只能有一个抽象方法
}

元注解

元注解为修饰其他注解的注解,在创建自定义注解的时候及其有用。下面我们介绍下JDK中的元注解。

@Retention

@Retention指明注解是如何被存储的。其参数有3个选项:

  • RetentionPolicy.SOURCE 仅在源代码中出现,注解会被编译器忽略。
  • RetentionPolicy.CLASS 该注解在编译时会被编译器读取。但会被JVM忽略。
  • RetentionPolicy.RUNTIME 注解在运行时会被JVM获取到。能够使用Java的反射API读取。这个选项使用的最为广泛。

@Target

@Target是用来限制注解使用的范围。它的参数是ElementType。下面贴出ElementType的代码:

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    // 用于修饰class, interface,@interface和enum类型声明
    TYPE,

    /** Field declaration (includes enum constants) */
    // 修饰成员变量,包括enum中的常量
    FIELD,

    /** Method declaration */
    // 方法声明
    METHOD,

    /** Formal parameter declaration */
    // 参数声明,比如Spring MVC中的@RequestParam
    PARAMETER,

    /** Constructor declaration */
    // 构造函数
    CONSTRUCTOR,

    /** Local variable declaration */
    // 局部变量
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    // 注解类型声明
    ANNOTATION_TYPE,

    /** Package declaration */
    // 包声明
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    // 用于泛型类型,例如class A<@Annotation T> {}
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    // 经试验,除了package和返回值为void的方法,其他位置都可以使用
    TYPE_USE
}

@Documented

@Documented注解表明使用Java Doc工具的时候,被该注解修饰的注解会出现在生成的Javadoc中。

@Inherited

标记为@Inherited的注解,修饰的class被其他类继承之时,该注解能够一并继承过去。下面以一段代码为例:
定义一个annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited //启用了注解继承
public @interface MyAnnotation {
}
public class Test {
    public static void main(String[] args) {
        System.out.println(B.class.isAnnotationPresent(MyAnnotation.class)); //@MyAnnotation修饰的是class A,class B继承自class A,因此class B是被MyAnnotation修饰的
        if (B.class.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation1 = B.class.getDeclaredAnnotation(MyAnnotation.class);
            System.out.println(annotation1); //返回null。B没有直接被MyAnnotation修饰
            MyAnnotation annotation2 = B.class.getAnnotation(MyAnnotation.class);
            System.out.println(annotation2); //返回@com.paultech.MyAnnotation()。
        }
    }
}

@MyAnnotation
class A {
}

class B extends A {
}

@Repeatable

@Repeatable表示该注解可以修饰同一元素多次。即能够像如下这种方式使用:

@Descripor("Hello")
@Descripor("World")
class SomeClass {}

下面介绍下如何定义自己的repeatable注解。

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(DoSomethingList.class) //需要指定一个容器注解
@interface DoSomething {
}

@Retention(RetentionPolicy.RUNTIME)
@interface DoSomethingList {
    DoSomething[] value(); // 这里必须为value
}

通过反射获取注解:

@DoSomething
@DoSomething //这里使用两个DoSomething
public class RepeatableTest {
    public static void main(String[] args) {
        DoSomething[] declaredAnnotationsByType = RepeatableTest.class.getDeclaredAnnotationsByType(DoSomething.class);
        System.out.println(declaredAnnotationsByType.length); // 输出为2
    }
}

自定义注解

Annotation的定义

注解使用@interface 定义,语法如下:

public @interface MyAnnotation {
    // ...属性定义
}

属性定义的语法为:

类型 字段名() [default] [defaultValue]

例如:

public @interface MyAnnotation {
    String value();
}

使用value作为属性名比较特殊,调用时可以显式指定属性名,也可以不指定:

@MyAnnotation(value = "Hello") // 显式指定value
class SomeClass {}

@MyAnnotation("world") // 不指定,默认为value属性
class AnotherClass {}

有一点需要格外注意的是,属性定义的字段类型必须为Java基本数据类型,再加上String和注解类型本身,或者他们的数组。

@interface Something {
    String[] strArr(); // String数组,合法
    int age(); // int类型,合法
    Integer someInt(); // 其他引用类型,不合法
    Another another(); // 注解类型,合法
}

@interface Another {}

如果定义了数组类型的属性,使用该注解时可以通过如下语法传入值:

@MyAnnotation(key = {"Hello", "World"})
class SomeClass {}

@MyAnnotation(key = "Hi Paul") //尽管key是String[]类型,如果只想传入一个值,仍然可以通过这种方式
class Another {}

注解相关的反射API

上文提到过,注解被指定为RUNTIME的时候,可以通过Java反射,在运行时获取到该注解。
以一段代码为例:

@SendSomething("Wahaha")
public class GetAnnotationDemo {

    public static void main(String[] args) {
    // 获取修饰GetAnnotationDemo的SendSomething类型注解
        SendSomething declaredAnnotation = GetAnnotationDemo.class.getDeclaredAnnotation(SendSomething.class);

        System.out.println(declaredAnnotation.value()); // 返回Wahaha
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface SendSomething {
    String value() default "defaultValue";
}

以上是注解最简单的使用。与注解有关的其他反射方法总结如下:

方法名 描述
isAnnotationPresent(A.class) 是否被A类型注解修饰
getDeclaredAnnotation(A.class) 获取类型为A的直接修饰的注解实例
getDeclaredAnnotationsByType(A.class) 获取类型为A的直接修饰的注解实例, 返回数组
getDeclaredAnnotations() 获取所有直接修饰的注解实例,返回数组
getAnnotation(A.class) 获取类型为A的注解实例,返回数组
getAnnotationsByType(A.class) 获取所有类型为A的注解实例,返回数组
getAnnotations() 获取所有注解实例,返回数组

注解的使用场景

在这个例子中我们要实现读取properties文件的内容并自动注入到class当中。
conf.properties文件:

username=paul
password=123456
@PropertySource("/path/to/conf.properties")
class UserConf {
    // 自动读取出username和password
    String username;
    String password;
}

下面代码给出了实现该功能的主要逻辑。
定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PropertySource {
    String value();
}

编写主要逻辑:
PropertyResolver.java

public class PropertyResolver {
    public  T getProperty(Class propertySourceClass) throws Exception {
        T propertySourceBean = propertySourceClass.newInstance();

        // 如果propertySourceClass被PropertySource注解修饰
        if (propertySourceClass.isAnnotationPresent(PropertySource.class)) {
            
            PropertySource propertySource = propertySourceClass.getDeclaredAnnotation(PropertySource.class);

            File propertyFile = new File(propertySource.value());
            // 读取properties文件内容到properties
            FileReader fileReader = new FileReader(propertyFile);
            Properties properties = new Properties();
            properties.load(fileReader);
            
            // 装配属性
            // 获取propertySourceClass所有的成员变量
            Field[] declaredFields = propertySourceClass.getDeclaredFields();
            // 获取属性文件中所有的key
            Set propertyNames = properties.stringPropertyNames();

            for (Field declaredField : declaredFields) {
                String fieldName = declaredField.getName();
                for (propertyName: propertyNames) {
                    if (fieldName.equals(propertyName)) {
                        // 如果成员变量不可访问,设置为能够访问
                        if (!declaredField.isAccessible()) {
                            declaredField.setAccessible(true);
                        }
                        // 设置属性值
                        declaredField.set(propertySourceBean, properties.getProperty(propertyName));
                        break;
                    }
                }
            }
        } 
        return propertySourceBean;
    }
}

使用自己编写的工具

public static void main(String[] args) {
    UserConf userConf = new PropertyResolver().getProperty(UserConf.class);
    userConf.username; // "paul"
    userConf.password; // "123456"
}

完整代码实现请点击链接: https://github.com/paul8263/PropertyResolver

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

参考资料

Oracle Predefined annotation types. https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html

你可能感兴趣的:(Java 注解的使用)