注解和注释的联系与区别
注解和注释都有对目标(类、方法、参数等)进行补充说明的作用,它们的区别在于:
- 注释只存在于类文件中,不参与编译;注解可以参与编译,也可以不参与编译。
- 注释内容仅供开发者和用户在编写程序时查看,对程序本身不会造成任何影响;注解可以在程序运行时利用反射读取,可以对程序本身造成影响。
java.lang中定义的三种注解
java.lang中定义了三种常用的注解:
- @Override:表示当前的方法定义将覆盖超类中的方法。如果把方法的名字拼错,或是参数列表与超类不一致(变成了重载),编译器就会发出错误提示。
- @Deprecated:表示该方法已经被废弃。如果在代码中使用被@Deprecated注解的方法,编译器会发出警告。
- @SuppressWarnings:关闭不当的编译器警告信息。
标记注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
}
上面的代码段中就定义了一个最简单的自定义注解。这个注解中没有任何元素,因此被称为标记注解。可以看出,注解的定义和接口很像,仅仅是在interface关键字前面加上了一个@。与类和接口的不同的是,在创建注解时需要加上两个元注解:@Target与@Retention。元注解是Java系统提供的,负责新注解的创建。
@Target
@Target用于定义该注解将应用于什么地方,可以是类、方法、域、参数等等。它的取值(存在于枚举类ElementType中)有以下几种:
- TYPE:类、接口(包括注解)、枚举
- FIELD:域、枚举实例
- METHOD:方法
- PARAMETER:参数声明
- CONSTRUCTOR:构造器
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解
- PACKAGE:包
- (Java 8)TYPE_PARAMETER:类型变量的声明。例:String str= new @NotNull String();
- (Java 8)TYPE_USE:使用类型的任何语句(声明语句、泛型和强制转换语句中的类型)。例:myString = (@NonNull String) str;
@DBTable被@Target(ElementType.TYPE)标注,因此它可以被用在类、接口、枚举上。
@Retention
@Retention用于定义注解在哪个级别可用。它的取值(存在于RetentionPolicy枚举类中)有以下几种:
- SOURCE:注解会被编译器抛弃
- CLASS:注解在class文件中可用,但会在运行时被VM抛弃
- RUNTIME:注解在运行期也将被保留,可以利用反射读取
@DBTable被@Retention(RetentionPolicy.RUNTIME)标注,因此它可以存在于运行期。
注解的简单使用
@DBTable
public class Table {
...
}
这样就给Table类加上了@DBTable注解。可以看出,注解的使用几乎和其他修饰符(public、static等)相同。
现在,这个注解还没有任何意义,后面会演示它的作用。
为注解添加元素
注解中可以添加以下类型的元素:
- 所有基本类型(int、long)
- String
- Class
- enum
- Annotation
- 以上类型的数组
下面为@DBTable注解添加一个String类型的tableName元素:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String tableName() default "";
}
注意:元素名称后面要加上一对圆括号。这是因为注解本身是一种继承自接口java.lang.annotation.Annotation的特殊接口。
可以利用default为元素设置默认值,当使用注解时没有给出该元素的值时,会使用预先设置的默认值。如果元素没有提供默认值,那么在使用时就必须给定一个值,否则编译器会报错。
有元素的注解的使用
@DBTable(tableName = "Table1")
public class Table {
}
这样就将@DBTable中的tableName设置成了“Table1”。
如果注解有多个元素需要设置,只需用逗号分隔开:
@User(id = 100, name = "Bob")
如果某个元素是数组,那么需要加上一对大括号:
@SuiteClasses({
Test1.class,
Test2.class,
Test3.class
})
上面的例子中还有一个特殊的地方:前面在为元素赋值时都使用“名 = 值”的模式,而这里没有显式标明元素名。实际上,这个是Java系统提供的一种“快捷方式”:如果注解中某个元素是唯一需要赋值的元素(唯一一个没有提供默认值的元素,或者是唯一一个元素),并且该元素值名为value,那么在赋值时就可以省去元素名与等号。
下面是SuiteClasses的定义,可以看到它只有一个元素,并且名称为value:
public @interface SuiteClasses {
public Class<?>[] value();
}
注解处理器实例:根据JavaBean生成SQL语句
相比于注释,注解的最大优点就是可以在运行时通过反射读取其内容,这为编写各种工具类乃至框架提供了极大的便利。下面的例子中提供了一个工具类以及一系列注解,可以根据满足JavaBean规范的类动态地生成SQL语句。
首先是使用到的自定义注解:
(1)表名注解@DBTable:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String tableName() default "";
}
(2)整型注解@SQLInteger:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
}
(3)字符串型注解@SQLString:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
public String name() default "";
public int length();
}
下面是注解处理器,基本都是通过反射读取注解内容实现。需要注意的一点是,JavaBean模式下所有域的访问权限都是private的,因此需要利用reflect包内的AccessibleObject类提供的方法获取其访问权限。
public class TableCreater {
public static String printSQL(Class<?> clazz){
//判断clazz是否有DBTable注解,若没有则返回
DBTable dbTable;
if(clazz == null || (dbTable = clazz.getAnnotation(DBTable.class)) == null) {
return null;
}
//获得表名
String tableName = dbTable.tableName().length() < 1 ?
clazz.getName().toUpperCase() : dbTable.tableName() .toUpperCase();
//获得所有域及其访问权限
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
//生成SQL语句CREATE TABLE中每一行的内容
List<String> columns = new LinkedList<>();
for(Field field : fields) {
String column = null;
Annotation[] annotations = field.getDeclaredAnnotations();
if(annotations.length < 1) {
continue;
}
for(Annotation annotation : annotations) {
if(annotation instanceof SQLInteger) {
SQLInteger sInteger = (SQLInteger)annotation;
String name = sInteger.name().length() < 1 ?
field.getName().toUpperCase() : sInteger.name().toUpperCase();
column = name + " " + "INT";
}else if (annotation instanceof SQLString) {
SQLString sString = (SQLString)annotation;
String name = sString.name().length() < 1 ?
field.getName().toUpperCase() : sString.name().toUpperCase();
String type = "VARCHAR(" + String.valueOf(sString.length()) + ")";
column = name + " " + type;
}
}
if(column != null){
columns.add(column);
}
}
//将表名和各行内容拼接成SQL语句
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE ");
sql.append(tableName);
sql.append("(\n");
for(int i = 0 ; i < columns.size() ; i++){
sql.append(" ");
sql.append(columns.get(i));
if(i < columns.size() - 1){
sql.append(',');
}
sql.append('\n');
}
sql.append(")\n");
//返回结果
return sql.toString();
}
}
下面看一个运行实例。首先是需要生成SQL语句的UserInfo类:
@DBTable(tableName = "USER")
public class UserInfo {
public UserInfo() {
}
@SQLString(length = 10)
private String name;
@SQLInteger
private int age;
@SQLString(length = 30)
private String job;
@SQLString(length = 30)
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
将UserInfo.class作为参数传入TableCreater.printSQL方法中:
public static void main(String[] args) {
System.out.println(TableCreater.printSQL(UserInfo.class));
}
输出结果:
CREATE TABLE USER(
NAME VARCHAR(10),
AGE INT,
JOB VARCHAR(30),
EMAIL VARCHAR(30)
)
@Documented与@Inherited
@Documented与@Inherited也是创建自定义注解时使用的元注解,它们的意义如下:
- @Documented:将此注解包含在Javadoc中。
- @Inherited:允许子类继承父类的注解。
(Java 8)重复注解
一般的注解不能在一个目标上重复使用。不过,在Java 8中新增了一个@Repeatable注解,通过它可以实现重复注解。使用方法如下:
(1)创建一个需要实现重复注解功能的注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface User {
String value();
}
(2)创建一个注解容器:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Users {
User[] value();
}
(3)为(1)中的注解加上@Repeatable并赋值,值为(2)中的注解容器的Class对象:
@Repeatable(Users.class)
使用示例:
@User("John")
@User("Bob")
public class SomeClass {
}