Annotation的分类
注解为JDK1.5引入的新内容,调用形式为@Annotation
。
注解的本质是接口。
注解不影响Java代码的执行。但在运行时,可以通过一些手段例如反射,获取注解的信息,并对其进行处理。
Annotation分为如下3类:
- JDK系统注解
- 元注解
- 自定义注解
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
网友评论