目录
1.什么是注解
从 JDK 1.5 开始在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),是 Java 中非常重要的一部分。注解都是 @ 符号开头的,例如我们在学习方法重写时使用过的 @Override 注解。
@Override
public String toString() {
return this.hashCode()+"";
}
上面的代码重写了 Object 类的 toString( ) 方法并使用了 @Override 注解。如果不使用 @Override 注解标记代码,程序也能够正常执行。那么这么写有什么好处吗?事实上,使用 @Override 注解就相当于告诉编译器这个方法是一个重写方法,如果父类中不存在该方法,编译器便会报错,提示该方法没有重写父类中的方法。这样可以防止不小心拼写错误造成麻烦。
同 Class 和 Interface 一样,注解也属于一种类型 。注解并不能改变程序的运行结果,也不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有些则可以在运行时读写字节码文件信息。
注解和注释都是用来描述程序的,但不同点就在于注解是编译器能够理解的,是给编译器看的,因此具有一定的格式限制;而注释是给阅读代码的人看的,因此可以随意编写,对代码没有任何影响。
注解可以用元数据这个词来描述,即一种描述数据的数据。所以可以说注解就是源代码的元数据。
2.注解的作用
(1)生成帮助文档。这是最常见的,也是 Java 最早提供的注解。
(2)在编译时进行格式检查。例如上面所提到的 @Override 注解;
(3)跟踪代码依赖性,实现替代配置文件功能。比较常见的是 Spring 2.5 开始的基于注解配置,作用就是减少配置量。现在的框架基本都使用了这种配置来减少配置文件的数量。
3.注解的使用
到 Java 8 为止 Java SE 提供了 11 个内置注解。其中有 5 个是基本注解,它们来自于 java.lang 包;有 6 个是元注解,负责注解其他的注解,它们来自于 java.lang.annotation 包,自定义注解时会用到元注解。
(1)基本注解的使用
① @Override 注解:用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。
② @Deprecated 注解:用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时。当使用已过时的元素时,编译器将会给出警告(过时元素中间有一横线)。
③ @SuppressWarnings 注解:指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素。通常情况下,如果你确认程序中的警告没有问题,可以不用理会。
④ @SafeVarargs 注解:与 @SuppressWarnings ("unchecked") 作用相同,取消未检查不安全代码的警告。
⑤ @FunctionalInterface:用来指定某个接口必须是函数式接口,所以 @FunInterface 只能修饰接口,不能修饰其它程序元素。
(2)自定义注解
注解的本质就是一个接口,该接口默认继承了 Annotation 接口,但是注解的定义格式又不同于普通的接口:
① 在定义注解时要在 interface 前加上@,表示这是一个注解;
② 注解的属性为接口的无参抽象方法,并且返回值只能为基本类型、String、枚举类以及以上类型的数组;
③ 在使用注解时必须给里面的属性赋值(格式:属性名 = 属性值)。若不想赋值,则必须在定义注解时属性后注明该属性的默认值;
④ 若属性只有一个且属性名为 value ,则使用时无需写属性名,直接传入值即可。
下面我们来声明一个自定义的注解并使用它:
/**
* 定义一个注解
*/
public @interface MyAnnotation {
//给注解添加属性
String name();//String 类型
int age();//基本数据类型
Enum pet() default Enum.Dog;//枚举类型,默认值为Dog
}
/**
* 使用自定义注解
*/
//pet有默认值,可以不写
//数组赋值要使用大括号包裹
@MyAnnotation(name = "张三",age = 25,sonName = {"张大大","张小小","张巧巧"})
public class Start {
//类中的内容
}
(3)元注解
JDK 1.5 定义了 4 个注解,分别是 @Documented、@Target、@Retention 和 @Inherited。JDK 1.8 又增加了@Repeatable 和 @Native 两个注解,在此只介绍前四种常用注解的使用。
① @Documented 注解:用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。
② @Target 注解:用来指定一个注解的使用范围,即被 @Target 修饰的注解可以用在什么地方。@Target 注解有一个成员变量(value)用来设置适用目标,value 是 java.lang.annotation.ElementType 枚举类型的数组,下表为 ElementType 常用的枚举常量。
名称 | 说明 |
---|---|
CONSTRUCTOR | 用于构造方法 |
FIELD | 用于成员变量(包括枚举常量) |
LOCAL_VARIABLE | 用于局部变量 |
METHOD | 用于方法 |
PACKAGE | 用于包 |
PARAMETER | 用于类型参数(JDK 1.8新增) |
TYPE | 用于类、接口(包括注解类型)或 enum 声明 |
③ @Retention 注解:用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示。
名称 | 说明 |
---|---|
SOURCE | 在源文件之后失效 |
CLASS | 在 class 文件之后失效 |
RUNTIME | 在运行完成后失效 |
④ @Inherited 注解:是一个标记注解,用来指定该注解可以被继承。如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
下面我们对在第 (2) 小节自定义的注解加上这些元注解限制:
/**
* 定义一个注解
*/
@Documented//声明javadoc文档中会显示注解信息
@Target(ElementType.TYPE)//声明该注解作用于类
@Retention(RetentionPolicy.RUNTIME)//声明该注解持续作用至运行结束
@Inherited//声明该注解可以被被声明者的子类继承
public @interface MyAnnotation {
//给注解添加属性
String name();//String 类型
int age();//基本数据类型
Enum pet() default Enum.Dog;//枚举类型,默认值为Dog
String[] sonName();//数组类型
}
/**
* 使用自定义注解
*/
//该注解此时可被Demo1的子类继承
@MyAnnotation(name = "张三",age = 25,sonName = {"张大大","张小小","张巧巧"})
public class Demo1 {
//编译时报错,只能用于修饰类,不能修饰方法
//@MyAnnotation(name = "张三",age = 25,sonName = {"张大大","张小小","张巧巧"})
public void method(){
System.out.println("方法执行");
};
}
并且在 Demo1 生成的帮助文档中可以看到该注解的相关信息:
4.解析注解——模拟工厂模式
至此我们已经学会了如何定义一个注解以怎么去使用一个注解,但对于自定义的注解,如果只是单纯的在类,方法或者属性上标注这些注解信息,那有什么意义呢?看起来就像是多此一举,不如注释来的直接。
俗话说得好:“存在即合理”,注解也是如此。上面曾提到过,注解是给编译器看的,因此注解中的信息就一定能够被编译器所读取。如果能够通过一种方式获取注解的信息,是不是就可以用注解来完成很多的事情?目前所流行的 Spring 系列框架正是这样做的,其使用注解代替了原来的配置文件,简化了配置文件的复杂步骤,虽然这样做加深了项目的耦合度,但同时也具有其独特的优势。
那么如何才能获取注解中的信息呢?反射机制中所用到的 Class 类就为我们提供了获取注解的方法 —— getAnnotation ( ) 。不止是 Class ,与其相关的 Field、Constructor 以及 Method 都为我们提供了此方法来获取注解。
下面我们先实现一个小案例 —— 模拟工厂模式:
① 声明一个名为 Bean 的注解,其中有一个 String 属性:className ;声明此注解只能作用在类上,并且有效期直到运行结束。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
String className();
}
② 声明一个只含有默认私有化构造方法的 Student 类,限制随意实例化,只能通过 Factory 实例化。
public class Student {
private String name;//姓名
private Integer age;//年龄
private Student(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
③ 声明一个被 Bean 注解修饰的 Factory 类,用于实例化没有属性特征的指定对象。
//配置需要实例化的类名称
@Bean(className = "com.java.day04.Student")
public class Factory {
public static Object getInstance() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取Factory的Class对象
Class<Factory> factoryClass = Factory.class;
//获取Factory的Bean注解
Bean bean = factoryClass.getAnnotation(Bean.class);
//获取注解的className属性
String className = bean.className();
//根据属性值生成Class对象
Class<?> instanceClass = Class.forName(className);
//通过Class对象获取Constructor对象
Constructor<?> constructor = instanceClass.getDeclaredConstructor();
constructor.setAccessible(true);//暴力反射私有构造方法
//返回Object类型的对象
return constructor.newInstance();
}
}
④ 此时我们是无法直接实例化一个学生对象的,因此只能使用 Factory 工厂模式进行实例化。
public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, InstantiationException,
IllegalAccessException {
//通过工厂模式实例化一个学生
Student student = (Student)Factory.getInstance();
//为学生注入属性
student.setName("张三");
student.setAge(20);
//打印学生信息
System.out.println(student);
}
运行结果:
Student{name='张三', age=20}
至此我们就完成了一个工厂模式的模拟,但是在模拟过程中有一个问题:我们知道注解是一个接口,那么下面这行代码又是如何实现的呢?
//获取Factory的Bean注解
Bean bean = factoryClass.getAnnotation(Bean.class);
其实是 JVM 自动为我们在内存中创建了一个 Bean 的实现类对象,并把它指向 bean 。该过程与下面代码过程完全一致:
Bean bean = new Bean(){
@Override
public String className(){
return "com.java.day04.Student";
};
}
其次还需要注意的是:还有一个获得类所有注解而并非指定注解的 getAnnotations ( ) 方法,返回的是该类所有注解的 Annotaion 数组。
Object[] objects = factoryClass.getAnnotations();