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<String> myList = new MyList<>();

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

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

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

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

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

@FunctionalInterface
interface MyForEachFunction<T> {
    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> T getProperty(Class<T> 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<String> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容