Java 基础之注解

xiaoxiao2021-02-27  283

从Java 1.5 开始,Java增加了对元数据(MetaData)的支持,也就是 Annotation (即注解)。它是代码里的一种特殊标记,注解标记可以在编译、类加载、运行时被读取,并执行相应操作。通过注解,开发者可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息,编译器也可以通过这些补充信息进行验证或进行部署。

Annotation 就像修饰符一样,可用于修饰包、类、构造方法,方法、成员变量、参数、局部变量的声明,这些信息都被存在 Annotation的“name = value”对中。

Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据,但是它并不影响程序代码的执行,无论增加或者删除 Annotation,代码都同样执行,如果希望Annotation起作用,需要通过 APT(Annotation Processing Tool)工具对这些信息进行访问和处理。

1、基本 Annotation

使用 Annotation 时要在前面添加 @ 符号,把该 Annotation 当作一个修饰符使用,用于修饰它支持的程序元素。 Java 提供了 5 个基本的 Annotation : @Override @Deprecated @SuppressWarnings @SafeVarargs(Java7 新增) @FunctionalInterface(Java8 新增)

1.1 @Override

覆写父类方法,可以强制一个子类必须覆盖父类的方法。它的作用主要是保证父类必须包含这个重写方法的方法。假如我们重写了一个方法,不小心写错或者写漏了一个字母,这种情况不会报错,编译器会理解为我们新增一个方法,在写了 N 多行代码后,我们一运行发现没有达到预期效果,但是这样的细微错误是很难发现的,@Override 就是保证父类必须有这个方法,如果我们写错了方法名就会在编译时立即报错。

例如准备重写 toString() 方法,结果把 S 小写了,程序不会报错,当我们打印对象的时候,发现明明是重写了 toString() 方法呀,结果还是打印的内存地址:

class Person { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String tostring() { return "姓名:" + name + ", 年龄:" + age; } } public class AnnotationDemo { public static void main(String[] args) { Person mPerson = new Person("张三", 24); System.out.println(mPerson); } }

打印如下:

这就是我们自以为重写了,其实是写错了,如果加上 @Override ,那么编译时就会报错:

The methodtostring() of type Person must override or implement a supertype method:意思是必须覆写的是父类的方法,也就是说父类中没有 tostring() 方法。

1.2 @Deprecated

假如我们在开发一个 SDK 的时候,刚开始设计了一个方法能够很完美的解决某个问题,随着程序的迭代,发现之前版本的该方法已经不能完全满足需求了,我们就需要重新设计一个方法,那么这时候就面临两种选择,一是直接删除之前的方法并加入新方法,二是保留之前的方法并加入新方法,同时提示之前的方法不推荐了。肯定是后者的方式更友好,让开发者心理有一个过渡期。我们可以使用一个 @Deprecated 来提示某个程序元素(类,方法等)已过时,当其他程序使用该类或方法时,编译器就会给出警告,开发者就应该去找新的方法啦。

可以看到加了 @Deprecated 修饰符的方法的方法名中间加了一道横线,调用的时候也有横线,所以当我们看到这横线的时候,就要想到这个方法是加了 @Deprecated 修饰符修饰的,已经弃用了,得去找新的方法了。

1.3 @SuppressWarnings

压制警告,它表示被该 Annotation 修饰的程序元素取消显示指定的警告,如果我们已经确定知道了某个警告的存在,虽然没有处理它,但是逻辑严密,保证了不会出错,那么编译器一直提示警告的话对于某些强迫症的人看起来就会很不舒服,在程序元素前加入 @SuppressWarnings 修饰符就可以取消显示这些警告。

例如 List 如果不指定泛型的话,就会出现警告:

如果在方法前加入 @SuppressWarnings 修饰符,就不会显示警告信息了:

1.4 @SafeVarargs

先看一段代码:

编译时正常,运行时不访问元素也不会出错,只要一访问 strList 中的元素就会出错,把一个不带泛型的对象赋给一个带泛型的变量时产生的泛型擦除错误,貌似称为“堆污染”。方法中出现了警告信息,调用方法的地方也出现了警告。方法中的警告可以添加 @SuppressWarnings 修饰符取消显示,但是调用方法的地方还有警告:

Type safety: Ageneric array of List<Integer> is created for a varargs parameter

@SafeVarargs 就是用来取消这个警告的,注意 @SafeVarargs 修饰符修饰参数可变 的方法(包括构造方法),修饰普通方法时还要加上static或者final修饰符:

1.5 @FunctionalInterface

Java 8 规定:如果接口中只有一个抽象方法(可包含多个默认方法或多个 static 方法),该接口就是函数式接口, @FunctionalInterface 就是用来指定某个接口必须是函数式接口,如下面这段代码,如果用 @FunctionalInterface 修饰了这个接口,当抽象方法大于一个时,就会报错了,提示该接口不是函数式接口了,但是添加 static 方法是没有问题的。@FunctionalInterface 只能修饰接口,不能修饰其他程序元素。

以上就是 5 个基本 Annotation 的用法了,其实使用开发工具,这些 Annotation 很少需要我们自己写,所以其实只要在看到这些 Annotation 的时候只要能够明白它是什么意思就行了。

2、Java 的元 Annotation

Java 8 除了 java.lang 包下提供了 5 个基本 Annotation 之外,还在 java.lang.annotation 包下提供了 6 个元 Annotation,其中有 5 个都用于修饰其他的 Annotation,而@Repeatable 是 Java 8 新增的重复注解。

@Retention

@Target

@Documented

@Inherited

@Native

@Repeatable

 

先放一个官方 Annotataion (@SafeVarargs)上来对比着看:

@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface SafeVarargs {}

2.1 @Retention

指定被修饰的 Annotation 保留多长时间,它的值有三个: RetentionPolicy.SOURCE:Annotation 只保留在源代码中,编译器将丢弃这种 Annotation。 RetentionPolicy.CLASS:默认值,Annotation 将被记录在 .class 文件中,运行 Java 程序时 JVM 不可获取 Annotation 信息。 RetentionPolicy.RUNTIME:Annotation 将被记录在 .class 文件中,运行 Java 程序时 JVM 也可获取 Annotation 信息,程序可以通过反射获得 Annotation 信息。 @Retention注解的写法可以写完整: @Retention(value = RetentionPolicy.RUNTIME) 当 Annotation 的成员变量名为 value 时,也可以像官方那样直接指定值,省略 “value=” : @Retention(RetentionPolicy.RUNTIME)

2.2 @Target

指定被修饰的 Annotation 能用于修饰哪些程序单元。它的值就比较多一点: ElementType.TYPE:表示该 Annotation 可以修饰类、接口(包括注解类型的接口)、枚举。 ElementType.FIELD:表示该 Annotation 可以修饰成员变量。 ElementType.METHOD:表示该 Annotation 可以修饰方法。 ElementType.PARAMETER:表示该 Annotation 可以修饰参数。 ElementType.CONSTRUCTOR:表示该 Annotation 可以修饰构造方法 ElementType.LOCAL_VARIABLE:表示该 Annotation 可以修饰局部变量。 ElementType.ANNOTATION_TYPE:表示该 Annotation 可以修饰 Annotation。 ElementType.PACKAGE:表示该 Annotation 可以修饰包。 ElementType.TYPE_PARAMETER:Java 8 新加入,表示该 Annotation 可以修饰类型参数。 ElementType.TYPE_USE:Java 8 新加入,表示该 Annotation 可以修饰类型使用。 @Target 的写法跟 @Retention 一样。

2.3 @Documented

指定被修饰的Annotation 将被 javadoc 工具提取成文档。如果定义 Annotation 类时使用 @Documented 修饰,则所有使用该Annotation 修饰的程序元素的 API文档中都会包含该Annotation 说明。

2.4 @Inherited

指定被修饰的Annotation 将具有继承性,如果某个类使用了 @Xxx 注解,而@Xxx 注解使用了 @Inherited 修饰,则这个类的的子类都将使用 @Xxx 注解。 上面四个比较常用。

2.5 @Native

这是一个比较特殊的注解,它不修饰 Annotation 的,元 Annotation,是用来标记Native 属性的,暂时没看到什么有用的资料。

2.6 @Repeatable

这是 Java 8 新增的重复注解,一会儿单独说这个注解。

了解了 6 个元 Annotation 后,我们就能看懂@SafeVarargs 是用来修饰方法和构造方法的,在运行时可以通过反射得到获取它的信息,而且将被 javadoc 工具提取成文档。明白每个元 Annotation 的含义,对看懂注解来说是必须的。

3、自定义 Annotation

了解了基本 Annotation 后,试着自定义 Annotation ,并利用它来完成一些实际功能。

 

3.1 基本定义

定义新的 Annotation 需要使用 @interface 关键字(在接口关键字 interface 前加 @ ):

public @interface MyAnnotation { }

这样就定义了一个新的Annotation 了,已经可以把它加在任意类、方法、成员变量等之前了。

可以给自定义的 Annotation 添加成员变量,成员变量是通过方法的形式添加:

public @interface MyAnnotation { // 成员变量是通过方法的形式来定义的 boolean sex(); int height(); }

成员变量可以添加默认值,使用 default 关键字: public @interface MyAnnotation { // 成员变量是通过方法的形式来定义的 boolean sex() default true; int height() default 170; }

注意,如果Annotation 添加了成员变量而没有默认值的话,在使用该 Annotation 时需要为其成本变量赋值:

@MyAnnotation(sex = true, height = 175)

如果有默认值,则可以不赋值,成员变量的值为默认值,如果赋值,成员变量的值为所赋的值,感觉 ButterKnife 就用到了这部分的知识。

 

3.2 如何使用

刚开始的时候提到添加了 Annotation 后不会自己生效,必须由开发者自己提取并处理 Annotation 信息,这里主要用到的是反射的知识,看来应该先去把反射弄明白的。而且要提取 Annotation 信息,之前学到的元 Annotation 就派上用场了。为自定义 Annotation 加入一个元注解 @Retention(RetentionPolicy.RUNTIME),让程序运行时可以通过反射拿到它的信息。

@Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { // 成员变量是通过方法的形式来定义的 boolean sex() default true; int height() default 170; }

我们先在一个类上添加这个注解:

@MyAnnotation(sex = true, height = 175) class Person implements IAction { //代码省略 }

然后通过反射技术拿到这个注解的信息,反射的知识还没深入,会另外记录,不过也了解点基本概念,这里看看通过反射获取注解的几个常用方法:

getAnnotation(Class<A>annotationClass):返回该程序元素上指定类型的注解,如果该类型的注解不存在,则返回null。

getAnnotations():返回该程序元素上的所有注解。

getAnnotationsByType(Class<A>annotationClass):Java 8 新增的方法,类似 getAnnotation() ,由于 Java 8 新增重复注解功能,所以该方法返回该程序元素上指定类型的多个注解。

getDeclaredAnnotation(Class<A>annotationClass):Java 8 新增的方法,获取直接修饰该程序元素上指定类型的注解,如果该类型的注解不存在,则返回null。

getDeclaredAnnotations():返回直接修饰该程序元素上的所有注解。

getDeclaredAnnotationsByType(Class<A>annotationClass):Java 8 新增方法,类似 getDeclaredAnnotation() ,由于 Java 8 新增重复注解功能,所以该方法返回该程序元素上指定类型的多个注解。

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

知道了这几个基本方法,就可以在程序运行时拿到 Annotation的信息了:

public class AnnotationDemo { public static void main(String[] args) { try { if (Class.forName("com.qinshou.annotation.Person").isAnnotationPresent(MyAnnotation.class)) { MyAnnotation myAnnotation = Class.forName("com.qinshou.annotation.Person") .getDeclaredAnnotation(MyAnnotation.class); System.out.println(myAnnotation.sex()); System.out.println(myAnnotation.height()); } } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

打印如下:

成功拿到在 Person 类上添加的自定义注解 MyAnnotation 的成员变量,并且成员变量的值不是默认值,是使用时赋的值。

如何获取在成员变量上添加的注解,原理跟获得类上添加的注解一样,就是要通过反射获取成员变量,不多说,贴个代码示例:

class Person implements IAction { private String name; @MyAnnotation(sex = true, height = 175) private int age; //代码省略 }

在age 成员变量上添加自定义注解。

public class AnnotationDemo { public static void main(String[] args) { try { Field[] fields = Class.forName("com.qinshou.annotation.Person").getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MyAnnotation.class)) { MyAnnotation myAnnotation = field.getDeclaredAnnotation(MyAnnotation.class); System.out.println(myAnnotation.sex()); System.out.println(myAnnotation.height()); } else { System.out.println(field.getName() + "没有添加MyAnnotation注解"); } } } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

打印结果如下:

3.3 小例子

我们也可以将 main() 方法中的代码提取出来单独作为一个类,稍微写个小例子:

@MyAnnotation:

@Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value(); }

Person:

class Person { @MyAnnotation(value = "张三") public String name; @MyAnnotation(value = "男") public String sex; @Override public String toString() { return "姓名:" + name + ",性别:" + sex; } }

MyAnnotationUtil:

public class MyAnnotationUtil { public static void precess(String clazz) { try { Field[] fields = Class.forName(clazz).getDeclaredFields(); Object object = Class.forName(clazz).newInstance(); for (Field field : fields) { if (field.isAnnotationPresent(MyAnnotation.class)) { MyAnnotation myAnnotation = field.getAnnotation(MyAnnotation.class); field.set(object, myAnnotation.value()); } } System.out.println(object.toString()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

main() 方法:

public class AnnotationDemo { public static void main(String[] args) { MyAnnotationUtil.precess("com.qinshou.annotation.Person"); } }

这里要注意Person 类中的成员变量不能为 private ,如何访问 private 的成员变量,在反射的会记录,还有forName() 方法接收的类名必须是全限定名(即要加上包名)。

我们只是在Person类加了注解,并没有创建对象什么的,就直接赋值了,有点 ButterKnife 的意思,具体操作就需要自己实现了,这只是记录了一个简单例子。

 

4、Java 8 新特性

4.1 @Repeatable

上面提到了这是 Java 8 新增的重复注解,在该注解出现之前,如果需要重复使用同样的注解编译器会报错,只能用以下方法通过声明一个注解数组来表示:

public @interface Authority { String role(); } public @interface Authorities { Authority[] value(); } public class RepeatAnnotationUseOldVersion { @Authorities({@Authority(role="Admin"),@Authority(role="Manager")}) public void doSomeThing(){ } }

这样写可读性比较差,违背了注解本身的意义。 Java 8 之后可以利用 @Repeatable 这样写: @Repeatable(Authorities.class) public @interface Authority { String role(); } public @interface Authorities { Authority[] value(); } public class RepeatAnnotationUseNewVersion { @Authority(role="Admin") @Authority(role="Manager") public void doSomeThing(){ } }

创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解,通过一个例子说明:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotations { MyAnnotation[]value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Repeatable(value = MyAnnotations.class) public @interface MyAnnotation { String name(); int age(); }

//旧方式 //@MyAnnotations({ @MyAnnotation(name = "张三", age = 24) //, @MyAnnotation(name = "李一", age = 25) }) @MyAnnotation(name = "张三", age = 24) @MyAnnotation(name = "李一", age = 25) public class AnnotationDemo { public static void main(String[] args) { MyAnnotation[] myAnnotations = AnnotationDemo.class.getDeclaredAnnotationsByType(MyAnnotation.class); for (MyAnnotation myAnnotation : myAnnotations) { System.out.println(myAnnotation.name() + "***" + myAnnotation.age()); } MyAnnotations myAnnotations2 = AnnotationDemo.class.getDeclaredAnnotation(MyAnnotations.class); System.out.println(myAnnotations2); } } 打印后半部分未截取完: MyAnnotations 可以称为 MyAnnotation  的“容器”,需要注意的是,“容器”保留期(@Retention)必须比其子元素要长,否则会报错。

4.2 ElementType.TYPE_PARAMETER和 ElementType.TYPE_USE

这两个枚举也是 Java 8 新增的,ElementType.TYPE_PARAMETER 表示在类型参数前可以添加此注解:

class Test<@MyAnnotation T> { } ElementType.TYPE_YSE 表示可以在类型名称的地方添加注解: //类之前使用注解 @MyAnnotation // implements时使用注解 public class AnnotationDemo implements @MyAnnotation Serializable { // 定义变量时使用注解 @MyAnnotation public static String string = ""; // 返回值为void的方法不能使用 public static void main(String[] args) { // 强转时使用注解 string = (@MyAnnotation String) new Object(); // 定义类型时使用注解 List<@MyAnnotation Integer> list = new ArrayList<>(); } // 有返回值的方法使用注解 @MyAnnotation // throws时使用注解 public int test() throws @MyAnnotation NullPointerException { return 0; } }

这两个新增的枚举可以让我们的注解用在更多的地方。

总结:关于注解还有更多的用法,还有编译时处理 Annotation ,这些都值得更深入。算起来为了了解上面这些知识,也是看了一整天才在脑子里有些印象了,累,但是也值得,基础真的是关键,不管学什么,就算你学得再快,框架用得再好,只要你还继续做程序猿,总有一天这些原理是你得弄明白的。对于泛型,注解,反射这三个点,我觉得注解应该最后看,因为注解用到了一些泛型和反射的知识。
转载请注明原文地址: https://www.6miu.com/read-6119.html

最新回复(0)