上篇詳細研究了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註解又多了可填的屬性,分別是since和forRemoval,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等,下面就簡單介紹下常見庫中的常見註解使用
JacksonJackson是一個通用的序列化庫,程式設計師使用過程中可以使用它提供的註解機制對序列化進行定製化操作,比如:
.使用@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
至此,一個簡單的格式化輸出類就完成了
萬水千山總是情,點個在看行不行