我們平常寫Java代碼,對其中的注解并不是很陌生,比如說寫繼承關系的時候經常用到@Override
來修飾方法。但是@Override
是用來做什么的,為什么寫繼承方法的時候要加上它,不加行不行。如果對Java的注解沒有了解過,很難回答這些問題。并且,現在越來越多的第三方庫開始使用注解,不了解注解的話很難理解他們的邏輯。趁著五一假期,趕緊補習一下什么是注解。
概況
注解是Java5之后引入的新特性,它與class
,interface
,enum
處于同一層次。可以理解為在代碼中插入一段元數據。它們是在實際的源代碼級別保存信息,而不是某種注釋性質的文字,這樣能夠使源代碼整潔,便于維護。它可以在三個時期起作用,分別是編譯時,構建時和運行時。他們可以在編譯時使用預編譯工具進行處理,也可以在構建時影響到Ant,Maven等打包工具,還可以在運行期使用反射機制進行處理。
基本用法
不帶參數:
@Override
public void onCreate(Bundle savedInstanceState){
//...
}
帶參數:
@CustomizeAnnotation( name = "wakaka", age = 22)
//...
只有一個參數(可以不指定字段名):
@CustomizeAnnotation("wakaka")
java自帶的標準注解:
-
@Deprecated
標記這個元素被棄用,如果在其它地方對它引用/使用,編譯器會發出警告信息。 -
@Override
表示當前的方法覆蓋父類中定義的方法。如果不小心拼寫錯誤,或者方法簽名對應不上父類的方法,編譯器會報出錯誤提示。 -
@SuppressWarnings
關閉警告信息。
定義注解
直接上例子:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestAnnotation {
}
使用方法:
public class MainAnnotation {
@TestAnnotation
public void testMethod() {
}
}
定義注解跟定義接口差不多,只不過關鍵字要換成 @interface
。定義注解時需要用到元注解,比如 @Target
, @Retention
。@Target
用來定義注解的使用位置,包括類,方法等;@Retention
定義注解在哪一個級別可用,源代碼、類、運行時。
注解中一般都包含某些元素來表示某些值。分析注解的時候,主程序或者構建工具可以獲取到這些信息。沒有元素的注解稱為標記注解,比如說@Override
@Deprecated
。
定義注解元素的方式類似于定義接口中的方法,區別在于可以為注解中的元素添加默認值。例子:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}
用法:
public class PasswordUtils {
@UseCase(id = 47, description = "Password must contain at least one numeric")
public boolean validatePassword(String password) {
return password.matches("\\w*\\d\\w");
}
@UseCase(id = 48)
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "New password can't equal previously used ones")
public boolean checkForNewPassword(List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
從上面的例子可以看到,元素的定義類似于方法的定義。方法名就是元素名。使用default
關鍵字可以為一個元素增加一個默認值。
使用的時候除了帶有默認值的元素,需要把所有的元素的值填滿。
元注解
Java目前內置了四種元注解。
-
@Target
表示該注解可以應用的地方。參數使用ElementType
:- CONSTRUCTOR 構造器的聲明;
- FIELD 域聲明;
- LOCAL_VARIABLE局部變量的聲明;
- METHOD方法聲明;
- PACKAGE包的聲明;
- PARAMETER參數聲明;
- TYPE 類、接口、注解、枚舉聲明;
-
@Retention
表示需要在什么級別保存該注解信息。參數使用RetentionPolicy
:- SOURCE注解將被編譯器丟棄;
- CLASS注解在class文件中使用,但是會被VM丟棄;
- RUNTIMEVM將在運行期也保留注解,因此可以通過反射機制讀取注解的信息。
-
@Documented
將此注解包含在Javadoc中。 -
@Inherited
允許子類繼承父類的注解
大多數時候,我們都需要定義自己注解,并編寫自己的處理器來處理他們。
注解元素
注解元素可用的類型如下:
- 所有基本類型(int, boolean, char, long, byte...)
- String
- Class
- enum
- Annotation
- 以上類型的數組
使用這些類型以外的類型會報錯。不允許使用
Integer
,Character
等包裝類型。
默認值的限制
- 所有元素要么有指定的值,要么有默認值;
- 非基本類型的值,無論是指定值還是默認值都不能用
null
。
注解不支持繼承
不能使用extend
來繼承某個@interface
類型。
編寫注解處理器
如果沒有讀取注解的邏輯,那注解跟注釋是差不多的。我們可以利用Java的反射機制構造注解處理器,或者利用工具apt解析帶有注解的Java源代碼。
例子:
public class UseCaseTracker {
public static void trackUseCases(List<Integer> usecases, Class<?> cl) {
for (Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getDeclaredAnnotation(UseCase.class);
if (uc != null) {
System.out.printf("Found Use Case: %d %s\n", uc.id(), uc.description());
usecases.remove(new Integer(uc.id()));
}
}
for (int i : usecases) {
System.out.printf("Warning: Missiong use case-%d", i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
UseCase
注解已經在之前的例子中定義。
這個例子功能就是簡單比較一下是否缺少一些沒有編寫的測試用例。其中useCases
列表包含了所有應當包含的測試用例,PasswodUtils
是所有UseCase
測試源代碼所在的類。
檢測過程中使用了反射方法getDeclaredMethods()
和getDeclaredAnnotation()
。先獲取PasswordUtils
類中的所有方法,并遍歷這個列表中的所有方法。如果一個方法被UseCase
注解修飾,獲取這個UseCase
對象,并取出它的所有元素值。打印UseCase的信息,并在useCases
中刪除這Usecase
編號。最后打印所有沒有編寫的用例編號。
生成信息
有些框架除了需要寫java代碼之外還需要一些額外的配置文件才能協同工作,這種情況最能體現出注解的價值。
比如說像EJB
,Hibernate
這樣的框架,一般都需要一份xml描述文件。他們提供了Java源文件中類和包的原始信息。如果沒有注解,我們在寫完java代碼之后需要額外再寫一份關于Java類的配置問文件。
如果我們想添加一個實體類,建立一份基本的的對象/關系的映射,達到自動生成數據庫表的目的。我們可以使用注解,它可以清晰的保存在Java源文件中,方便我們了解實體與的關系。
例子:
數據庫中的所有屬性都通過注解來傳遞,所以我們需要定義一些數據庫中的‘類型’。這里我們簡單的做一個例子,并沒有定義全部的屬性和類型。
//對應數據庫中的表, 只有一個屬性,表名;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}
//表中每個字段的約束,只寫了3個
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNull() default true;
boolean unique() default false;
}
//對應數據庫中的 INT 類型
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints contraints() default @Constraints();
}
//對應數據庫中的 VARCHAR 類型
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints();
}
/**
* Created by yuxiaofei on 2016/5/7.
* 實體類,被注解修飾的成員變量會被加入到數據庫中
*/
@DBTable(name = "Member")
public class Member {
@SQLString(value = 30)
String firstName;
@SQLString(value = 50)
String lastName;
@SQLInteger
Integer age;
@SQLString(value = 30, constraints = @Constraints(primaryKey = true))
String handle;
static int memberCount;
}
/**
* Created by yuxiaofei on 2016/5/7.
* 根據目標實體類,聲稱創建表的SQL
*/
public class TableCreator {
public static void main(String[] args) {
Class<?> targetClass = Member.class;
DBTable dbTable = targetClass.getAnnotation(DBTable.class);
if (dbTable == null) {
System.out.printf("No DBTable in class %s. \n", targetClass.getSimpleName());
System.exit(-1);
}
String tableName = dbTable.name();
if (tableName.length() < 1) {
//默認名使用類名的全字母全大寫
tableName = targetClass.getName().toUpperCase();
}
List<String> columnDefs = new ArrayList<String>();
getColumnDefs(targetClass, columnDefs);
String SQL = createSQL(tableName, columnDefs);
System.out.println(SQL);
}
//根據表名和字段聲明,生成創建表的SQL
private static String createSQL(String tableName, List<String> columnDefs) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("CREATE TABLE %s (\n", tableName));
for (int i = 0; i < columnDefs.size(); i++) {
String column = columnDefs.get(i);
if (i != columnDefs.size() - 1) {
sb.append(String.format(" %s,\n", column));
} else {
sb.append(String.format(" %s\n);", column));
}
}
return sb.toString();
}
/**
* 根據實體類生成創建表的字段
*
* @param targetClass 目標實體類
* @param columnDefs 字段聲明列表
*/
private static void getColumnDefs(Class<?> targetClass, List<String> columnDefs) {
for (Field field : targetClass.getDeclaredFields()) {
String columnName = null;
Annotation[] anns = field.getDeclaredAnnotations();
if (anns.length < 1) {
continue;//沒有被注解修飾,非數據庫表內字段
}
if (anns[0] instanceof SQLInteger) {
SQLInteger sqlInteger = (SQLInteger) anns[0];
if (sqlInteger.name().length() < 1) {//默認名,用變量名全大寫形式代替
columnName = field.getName().toUpperCase();
} else {
columnName = sqlInteger.name();
}
Constraints constraints = sqlInteger.contraints();
columnDefs.add(String.format("%s INT %s", columnName, getConstraints(constraints)));
} else if (anns[0] instanceof SQLString) {
SQLString sqlString = (SQLString) anns[0];
if (sqlString.name().length() < 1) {//默認名用變量名全字母大寫
columnName = field.getName().toUpperCase();
} else {
columnName = sqlString.name();
}
Constraints constraints = sqlString.constraints();
columnDefs.add(
String.format(
"%s VARCHAR(%d) %s",
columnName, sqlString.value(), getConstraints(constraints)
)
);
}
}
}
/**
* 根據注解中的配置聲稱字段的約束
*
* @param constraints 注解約束配置
* @return 字段約束
*/
private static String getConstraints(Constraints constraints) {
StringBuilder sb = new StringBuilder();
if (!constraints.allowNull()) {
sb.append(" NOT NULL");
}
if (constraints.primaryKey()) {
sb.append(" PRIMARY KEY");
}
if (constraints.unique()) {
sb.append(" UNIQUE");
}
return sb.toString();
}
}
輸出結果就是一條sql語句:
CREATE TABLE Member (
FIRSTNAME VARCHAR(30) ,
LASTNAME VARCHAR(50) ,
AGE INT ,
HANDLE VARCHAR(30) PRIMARY KEY
);
例子比較簡單,運行一下就可以看到結果。雖然創建一個實體的代碼變多了,但是以后每次添加一個實體,一張表都很方便。
對于注解的學習就到這里了,有什么疑問可以在回復中一起交流。
參考文獻: 《Java編程思想》