Java 注解 學習筆記

我們平常寫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目前內置了四種元注解

  1. @Target
    表示該注解可以應用的地方。參數使用ElementType:
    • CONSTRUCTOR 構造器的聲明;
    • FIELD 域聲明;
    • LOCAL_VARIABLE局部變量的聲明;
    • METHOD方法聲明;
    • PACKAGE包的聲明;
    • PARAMETER參數聲明;
    • TYPE 類、接口、注解、枚舉聲明;
  2. @Retention
    表示需要在什么級別保存該注解信息。參數使用RetentionPolicy
    • SOURCE注解將被編譯器丟棄;
    • CLASS注解在class文件中使用,但是會被VM丟棄;
    • RUNTIMEVM將在運行期也保留注解,因此可以通過反射機制讀取注解的信息。
  3. @Documented
    將此注解包含在Javadoc中。
  4. @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編程思想》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容