詳解Java註解機制

2021-02-19 享學課堂online

上篇詳細研究了Java中的反射操作以及Class類相關內容,但在Java開發過程中,除了反射,往往還有泛型、註解等相關特性操作組合使用來實現一些高級技術,如Spring中就大量使用了反射和註解,實現了諸如Bean容器管理機制等操作,SpringMvc框架中大量使用了註解,實現了servlet容器的簡易操作等,現在我們開始詳細的學習Java中的註解機制

註解是什麼

日常開發中經常提到註解,那麼註解是什麼呢?在Java中,註解就是給程序添加一些信息,用字符@開頭,可以用來修飾後續的其他代碼元素,比如類、接口、欄位、方法、參數、構造方法等,往往註解還搭配編譯器、程序運行時以及其他工具或者插件使用,用於實現代碼功能的增強或者修改程序的行為等操作

Java內置註解

在Java中內置了一些註解,用來在類、方法申明使用,從而實現編譯器檢查、避免編譯器檢查等操作,同時也提高了java邏輯的嚴謹性,而在Java中內置的常見註解莫過於@Override、@Deprecated、@SuppressWarnings三個,下面分別介紹這三個常見的java內置註解的作用

@Override

@Override註解修飾於方法上,表明當前方法不是當前類首先申明的,而是由父類或者接口中繼承來的方法,並且當前類進行了方法重寫操作,比如:

public class Base {

public void action() {};

}

現在有一個父類Base,申明一個方法--action,現在有一個子類繼承了Base類,並且重寫了action方法的內部實現,如下:

public class Child extends Base {

@Override

public void action(){

System.out.println("child action");

}

}

可以看到在Child類的action方法上有一個@Override註解,代表著此方法是Child類重新實現並且繼承於Base類,但是當我們把這個註解刪除,發現工程無論是編譯還是運行,和之前一般無二,那麼編譯器為什麼還要給我們默認添加這個註解呢?其實我們可以反過來思考,如果我們的類中有一個方法,並且使用了@Override註解進行修飾,但是當前類並沒有從別的類或者接口繼承來這個方法,或者此類本身就是獨立的類,這個時候會發生什麼呢?

public class Child extends Base {

@Override

public void action(){

System.out.println("child action");

}

//action1方法在Base中不存在

@Override

public void action1(){

System.out.println("child class");

}

}

很明顯當我們加上註解的瞬間,編譯器就會直接提示異常,要求我們刪除當前註解。從這可以看出來,@Override註解有助於幫助編譯器檢查語法錯誤,執行更嚴格的代碼檢查。

@Deprecated

@Deprecated註解可以修飾的範圍很廣,不僅可以作用在方法上,還可以修飾類、欄位以及參數等所有註解可以修飾的範圍,此註解代表被修飾的元素已經過時,並且警告使用者建議使用其他方法,不再使用當前方法。例如,Date類中就有很多方法被標識已經過時了:

@Deprecated

public Date(int year, int month, int date){

}

@Deprecated

public int getYear(){

}

當我們在使用這些方法的時候,ide往往會給我們加上一個刪除線用來輔助提示調用者此方法已經過時,隨時都可能在下個版本刪除,不建議使用。而在Java9中,Deprecated註解又多了可填的屬性,分別是sinceforRemoval,since屬性類型為String類型,表示被修飾的元素從哪一個版本號開始過時,不建議使用,而forRemoval屬性為Boolean類型,表示將來在過時的時候是否會刪除當前元素,標記為true代表將來會刪除此元素,使用者請慎用。而在Java9中,Integer包裝類的構造方法中就有使用此特性標識的,如下:

@Deprecated(since="9")

public Integer(int value) {

this.value = value;

}

@SuppressWarnings

@SuppressWarnings註解則是表示壓制Java中的編譯警告措施,使得編譯器放棄被修飾元素的編譯警告,此註解有一個必填參數,表示壓制的是哪種類型的警告,此註解也可以使用在幾乎所有元素中。比如我們在開發中使用了Date類中的一些過期或者有異常拋出風險的方法,ide往往就會添加一個警告的黃線,而我們在方法上添加@SuppressWarnings註解,則會發現警告消失不見,如下:

//添加在方法上,取消掉編譯器的嚴格檢查警告機制

@SuppressWarnings({"deprecation","unused"})

public static void main(String[] args) {

Date date = new Date(2019, 10, 12);

int year = date.getYear();

}

常見的庫中的註解

日常開發使用的庫中也有著大量的註解,例如Jackson、SpringMvc等,下面就簡單介紹下常見庫中的常見註解使用

Jackson

Jackson是一個通用的序列化庫,程式設計師使用過程中可以使用它提供的註解機制對序列化進行定製化操作,比如:

.使用@JsonIgnore和@JsonIgnoreProperties配置序列化的過程中忽略部分欄位

.使用@JsonManagedReference和@JsonBackReference可以配置實例之間的互相引用

.使用@JsonProperty和@JsonFormat配置序列化的過程中欄位名稱和屬性欄位的格式等

Servlet3.0

隨著web開發技術的發展,Java web已經發展到了Servlet3.0,在早期使用Servlet的時候,我們只能在web.xml中配置,但是當我們使用Servlet3.0的時候開始,已經開始支持註解了,比如我們可以使用@WebServlet配置一個類為Servlet類,如下:

@WebServlet(urlPatterns = "/async", asyncSupported = true)

public class AsyncDemoServlet extends HttpServlet {

//....

}

SpringMvc

同樣的,在web開發中,我們往往還會使用SpringMvc框架來簡化開發,其框架的大量註解可以幫助我們減少大量的業務代碼,例如一個請求的參數和欄位/實例之間的映射關係,一個方法使用的是Http的什麼請求方法,對應請求的某個路徑,同樣的請求如何解析,返回的響應報文格式定義等,這些都可以使用註解來簡化實現,一個簡單的Mvc操作如下:

@Controller

@RequestMapping("/hello")

public class HelloController {

@GetMapping("/test")

@ResponseBody

public String test(){

return "hello test";

}

}

其中@Controller註解標明當前的類是SpringMvc接管的一個Bean實例,@RequestMapping("/hello")則是代表當前Bean的前置請求路徑比如是/hello開頭, @GetMapping("/test")則是表示test方法被訪問必須是Http請求的get請求,並且路徑必須是/hello/test為路徑前置,@ResponseBody註解則是標明了當前請求的相應信息按照默認的格式返回(根據後綴名來確定格式)

註解的創建

從上面我們可以看到使用註解的確可以很方便的簡化我們開發過程,因此很多庫和開發過程中,也會使用大量的註解簡化開發,那麼這些註解我們如何實現呢?首先我們先看看最常見的註解@Override的創建:

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.SOURCE)

public @interface Override {

}

從上面我們可以看到,Override類中使用了@Target元註解和@Retention元註解來定義整個註解,@Target表示需要註解的目標,@Retention則是標明當前註解的信息可以保留到Java的什麼階段,而除了這兩個元註解以外,還有兩個元註解@Documented和@Inherited,@Documented用來表示當前的註解信息是否包含到文檔中,@Inherited註解則是和註解之間的繼承有對應的關係,那麼這些元註解具體有什麼作用,以及具體有哪些參數可以選擇呢?接下來我們便分別學習一下

@Target

@Target註解表示當前註解可以使用在什麼類型的元素上,這裡的值可以多選,即一個註解可以作用在多種不同類型的元素上,具體的可選值在ElementType枚舉類中,值如下:

取值解釋TYPE表示作用在類、接口上FIELD表示作用在欄位,包括枚舉常量中METHOD表示作用在方法中PARAMETER表示作用在方法中的參數中CONSTRUCTOR表示作用在構造方法中LOCAL_VARIABLE表示作用在本地常量中MODULE表示作用在部分模塊中(Java9引入的概念)ANNOTATION_TYPE表示當前註解作用在定義其他註解中,即元註解PACKAGE表示當前註解使用在包的申明中TYPE_PARAMETER表明當前註解使用在類型參數的申明中(Java8新增)TYPE_USE表明當前註解使用在具體使用類型中(Java8新增)

當使用多個作用域範圍的時候,使用{}包裹多個參數,比如@SuppressWarnings註解的Target就有多個,在Java7中的定義為:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})

@Retention(RetentionPolicy.SOURCE)

public @interface SuppressWarnings {

String[] value();

}

即代表SuppressWarnings註解均可以作用在這七種元素上

@Retention

@Retention註解則是表明了當前註解可以保留到Java多個階段的哪一個階段,參數類型為RetentionPolicy枚舉類,可取值如下:

取值解釋SOURCE此註解僅在原始碼階段保留,編譯後即丟失註解部分CLASS表示編譯後依然保留在Class字節碼中,但是加載時不一定會在內存中RUNTIME表示不僅保留在Class字節碼中,一直到內存使用時仍然存在

此註解有默認值,即當我們沒有申明@Retention的時候,默認則是Class取值範圍

@Documented

@Documented註解沒有具體的參數,使用此元註解,則表示帶有類型的註解將由javadoc記錄

@Inherited

@Inherited註解與註解的繼承有關係,具體關係為如果使用了當前的元註解,則表示此註解可以被其他的註解的子類直接繼承,但是需要注意的是對已實現接口上的註解將沒有作用。

我們通過一個案例來了解@Inherited註解的作用,首先我們定義一個Test註解:

@Inherited

@Retention(RetentionPolicy.RUNTIME)

public @interface Test {

}

Test註解上有@Inherited元註解修飾,則表明Test註解會被繼承,接著我們在Base類上使用Test註解:

@Test

public class Base {

}

這個時候實現一個子類Child,我們來通過反射類Class中的isAnnotationPresent方法列印,看此類中的Test是否存在且是來自於父類繼承而來:

public class Child extends Base {

}

public static void main(String[] args) {

System.out.println(Child.class.isAnnotationPresent(Test.class));//true

}

最後輸出的結果為true,則表明Child類中存在@Test註解,並且是由繼承父類而來

使用註解實現簡單定製序列化

上面我們已經學習了註解定義和創建相關的內容,接下來我們利用註解簡單實現通用格式化輸出的類SimpleFormatter,類中有一個方法format,方法參數為Object類型的實例對象,即表明了對象的序列化方式,類定義如下:

/**

* 通用格式轉換輸出類

*/

public class SimpleFormatter {

/**

* 通用格式化方法==>將obj對象輸出為String

* @param obj

* @return

*/

public static String format(Object obj){

try{

Class<?> cls = obj.getClass();

StringBuilder builder = new StringBuilder();

for (Field field : cls.getDeclaredFields()) {

if(!field.isAccessible()){

field.setAccessible(true);//放棄java安全檢測,設置可以訪問私有欄位

}

//獲取Label註解-輸出的欄位名稱

Label label = field.getAnnotation(Label.class);

String name = null == label ? field.getName() : label.value();

//獲取欄位對應的value

Object value = field.get(obj);

//如果是Date類型,走時間格式化

if(null != value && field.getType() == Date.class){

value = formatter(field,value);

}

builder.append(name + "?" + value + "\n");

}

return builder.toString();

}catch (Exception e){

e.printStackTrace();

throw new RuntimeException("格式化輸出失敗:"+e.getMessage());

}

}

/**

* 針對時間類型欄位進行格式化的方法

*/

private static Object formatter(Field field, Object value) {

Format format = field.getAnnotation(Format.class);

if(null == format){

return value;

}

String pattern = format.pattern();

String timezone = format.timezone();

SimpleDateFormat sdf = new SimpleDateFormat(pattern);

sdf.setTimeZone(TimeZone.getTimeZone(timezone));

return sdf.format(value);

}

}

而除了格式化的類以外,我們還定義了兩個註解,一個用來表明格式化的時候的欄位名稱,一個用來針對時間格式欄位輸出的格式,如下:

/**

* Labl表明當前欄位輸出的名稱,僅作用在欄位上

*/

@Retention(RUNTIME)

@Target(FIELD)

public @interface Label {

String value() default "";

}

--

/**

* Format註解作用在欄位上,針對時間欄位類型的輸出格式

*/

@Retention(RUNTIME)

@Target(FIELD)

public @interface Format {

String pattern() default "yyyy-MM-dd HH:mm:ss";

String timezone() default "GMT+8";

}

除此之外,我們還需要一個實例類:

public class Student {

@Label("姓名")

private String name;

@Label("出生日期")

@Format(pattern="yyyy/MM/dd")

private Date born;

@Label("分數")

private double score;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Date getBorn() {

return born;

}

public void setBorn(Date born) {

this.born = born;

}

public double getScore() {

return score;

}

public void setScore(double score) {

this.score = score;

}

}

定義完畢後,我們可以這麼來使用:

public static void main(String[] args) throws ParseException {

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

Student zhangsan = new Student();

zhangsan.setName("張三");

zhangsan.setBorn(format.parse("1990-12-12"));

zhangsan.setScore(655);

System.out.println(SimpleFormatter.format(zhangsan));

}

輸出的結果為:

姓名?張三

出生日期?1990/12/12

分數?655.0

至此,一個簡單的格式化輸出類就完成了


萬水千山總是情,點個在看行不行

相關焦點

  • 談談 Java 中自定義註解及使用場景
    Java自定義註解一般使用場景為:自定義註解+攔截器或者AOP,使用自定義註解來自己設計框架,使得代碼看起來非常優雅。本文將先從自定義註解的基礎概念說起,然後開始實戰,寫小段代碼實現自定義註解+攔截器,自定義註解+AOP。一.
  • 詳解Java泛型機制
    使用的時候的代碼只要這麼改動:Pair minmax = newPair(1,100);Integer min = (Integer)minmax.getFirst();//欄位強制轉換Integer max = (Integer)minmax.getSecond();//欄位強制轉換這樣使用其實是可以的,事實上Java提供的泛型機制其實底層就是如此實現的
  • SpringBoot使用@Transactional註解配置事務
    點擊上方藍色字體,選擇「標星公眾號」優質文章,第一時間送達  作者 |  宋曉華 來源 |  urlify.cn/2qe6ju66套java雖然@Transactional註解可以作用於接口、接口方法、類以及類方法上,但是Spring建議不要在接口或者接口方法上使用該註解,因為這隻有在使用基於接口的代理時它才會生效。另外,@Transactional註解應該只被應用到public方法上,這是由Spring AOP的本質決定的。
  • Java工程師成神之路~
    堆和棧區別Java內存模型內存可見性、重排序、順序一致性、volatile、鎖、final垃圾回收內存分配策略、垃圾收集器(G1)、GC算法、GC參數、對象存活的判定JVM參數及調優Java對象模型oop-klass、對象頭HotSpot即時編譯器、編譯優化類加載機制
  • Java筆試面試-SpringMVC 核心
    ,如下所示:註解運行時檢查@AssertFalse被註解的元素必須為 false@AssertTrue被註解的元素必須為 true@DecimalMax(value)被註解的元素必須為一個數字,其值必須小於等於指定的最大值@DecimalMin(Value)被註解的元素必須為一個數字,其值必須大於等於指定的最小值@Digits(integer=, fraction=)被註解的元素必須為一個數字
  • 一個註解搞定 SpringBoot 接口防刷,還有誰不會?
    作者:CS打贏你blog.csdn.net/weixin_42533856/article/details/82593123說明:使用了註解的方式進行對接口防刷的功能
  • 一個註解搞定接口防刷!還有誰不會?
    說明:使用了註解的方式進行對接口防刷的功能
  • 關於Java中枚舉Enum的深入剖析
    javac Season.java然後,我們利用反編譯的方法來看看字節碼文件究竟是什麼.這裡使用的工具是javap的簡單命令,先列舉一下這個Season下的全部元素.➜  company javap SeasonWarning: Binary file Season contains com.company.SeasonCompiled from "Season.java"public final class com.company.Season extends java.lang.Enum<com.company.Season>
  • java並發編程系列:wait/notify機制
    由於原子性操作、內存可見性和指令重排序的存在,java提供了volatile和synchronized的同步手段來保證通信內容的正確性,假如沒有這些同步手段,一個線程的寫入不能被另一個線程立即觀測到,那這種通信就是不靠譜的~wait/notify機制故事背景
  • 深入JVM--探索Java虛擬機的類加載機制
    這個過程稱作虛擬機的類加載機制。類加載機制是虛擬機中很重要的一部分內容,在面試中出現的頻率也是比較高。因此,作為一個 Java 程式設計師,JVM 的類加載機制是我們必須掌握的知識。② 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。③ 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 面試|JAVA 引用詳解
    本文先來介紹一下java的四種引用類型。一,四種引用介紹從Java SE2開始,就提供了四種類型的引用:強引用、軟引用、弱引用和虛引用。Java中提供這四種引用類型主要有兩個目的:第一是可以讓程式設計師通過代碼的方式決定某些對象的生命周期;第二是有利於JVM進行垃圾回收。
  • Java類加載器 — classloader 的原理及應用
    虛擬機把描述類的數據從class字節碼文件加載到內存,並對數據進行檢驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。了解java的類加載機制,可以快速解決運行時的各種加載問題並快速定位其背後的本質原因,也是解決疑難雜症的利器。因此學好類加載原理也至關重要。
  • 輕鬆看懂Java字節碼
    目光右移,0000是編譯器jdk版本的次版本號0,0034轉化為十進位是52,是主版本號,java的版本號從45開始,除1.0和1.1都是使用45.x外,以後每升一個大版本,版本號加一。也就是說,編譯生成該class文件的jdk版本為1.8.0。 通過 java -version 命令稍加驗證, 可得結果。
  • Java 8 中新的 Date 和 Time 類入門詳解
    java.time.LocalDate:  LocalDate只提供日期不提供時間信息。它是不可變類且線程安全的。  package org.smarttechie;  import java.time.LocalDate;  import java.time.temporal.ChronoUnit;  /**  * This class demonstrates JAVA 8 data and time API  * @author
  • SpringMVC:註解@ControllerAdvice的工作原理
    Spring MVC中,通過組合使用註解@ControllerAdvice和其他一些註解,我們可以為開發人員實現的控制器類做一些全局性的定製,具體來講,可作如下定製 :結合@ExceptionHandler使用 ==> 添加統一的異常處理控制器方法結合@ModelAttribute使用 ==> 使用共用方法添加渲染視圖的數據模型屬性
  • Java從零開始學- 第32天:高並發中計數器的實現方式有哪些?
    點擊上方關注 「Java研究所」設為「星標」,和你一起掌握更多資料庫知識這是java高並發系列第32篇文章。java環境:jdk1.8。方式一:synchronized方式實現package com.itsoku.chat32;import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.concurrent.CountDownLatch
  • Java 內存模型 JMM 詳解!
    由於上述這些多線程情況下的各種問題,多線程中的程序順序已經不是底層機制中的執行順序和結果,程式語言需要給開發者一種保證,這個保證簡單來說就是一個線程的修改何時對其他線程可見,因此Java語言提出了JavaMemoryModel即Java內存模型,對於Java語言、JVM、編譯器等實現者需要按照這個模型的約定來進行實現。
  • Java 內存模型 JMM 淺析
    由於上述這些多線程情況下的各種問題,多線程中的程序順序已經不是底層機制中的執行順序和結果,程式語言需要給開發者一種保證,這個保證簡單來說就是一個線程的修改何時對其他線程可見,因此Java語言提出了JavaMemoryModel即Java內存模型,對於Java語言、JVM、編譯器等實現者需要按照這個模型的約定來進行實現。
  • 理解Java和Android虛擬機
    將源碼編譯成字節碼class字節碼java的字節碼文件是通過java編譯器來生成的,我們下載jdk後,通過javac命令,就可以將一個java源文件生成java字節碼文件,這個字節碼文件就可以直接在JVM上面運行了。