前言
這個(gè)知識點(diǎn)計(jì)劃分3篇文章來講解。
1、java注解是什么?
注解是jdk1.5引入的一個(gè)新特性;
可以把它理解為一種能夠跟代碼綁定并且能夠存儲數(shù)據(jù)的技術(shù);
【與代碼綁定】指的是它直接依附在代碼上,然后【能夠存儲數(shù)據(jù)】是指注解里存放著能夠描述它所依附的代碼的信息;
所以簡單點(diǎn)理解注解就是被造出來存儲數(shù)據(jù)的,而它所存儲的數(shù)據(jù)又是用于描述它所依附的代碼元素的,它依附在哪里,描述的就是哪個(gè)代碼元素。
而我們用專業(yè)點(diǎn)的叫法的話,注解所存儲的那些數(shù)據(jù)我們稱之為java代碼元素的元數(shù)據(jù)。
2、Java的代碼元素是什么呢?
這個(gè)元素只是我自己命的名。
例如java的類、接口、枚舉、方法、屬性、參數(shù)都屬于java的代碼元素。
3、什么是元數(shù)據(jù)?
元數(shù)據(jù)解釋起來呢就是用來描述某種事物的數(shù)據(jù),這樣解釋可能會挺抽象的,那咱們換一種方式,直接舉個(gè)例子說明吧:
我們應(yīng)該都操作過文件吧,不管是linux系統(tǒng)還是windows系統(tǒng)的文件,文件的作用就是用來存儲數(shù)據(jù),那我們操作的時(shí)候如何分辨文件的"身份"呢,要怎樣把每個(gè)不同的文件區(qū)分開呢?可以給文件起個(gè)唯一的名字吧,這樣通過文件名我是不是就可以知道要操作的是哪個(gè)文件了;
再者我想要知道某個(gè)文件存儲的數(shù)據(jù)的字節(jié)大小是多少,那我該怎么辦呢?是不是要重新對文件從頭到尾讀一遍,然后計(jì)算出文件的字節(jié)數(shù);當(dāng)然不需要這樣做,我們可以在一開始寫文件的同時(shí)就對文件的大小進(jìn)行統(tǒng)計(jì),寫入完成后就把統(tǒng)計(jì)得到的文件的大小存儲起來,后續(xù)要拿文件的大小的時(shí)候,我們就直接獲取這個(gè)存儲起來的文件字節(jié)數(shù)就行了...
像文件名就是用來描述文件的"身份"的,用于區(qū)分不同的文件;文件的大小用來描述文件存儲的數(shù)據(jù)的大小;文件的修改時(shí)間用來描述文件最近一次修改發(fā)生在什么時(shí)候...
文件名、文件大小、文件的修改時(shí)間等都是一些數(shù)據(jù),而且是用來描述一個(gè)文件的數(shù)據(jù),那我們就會把這些數(shù)據(jù)稱做文件的元數(shù)據(jù)--即用于描述文件的一些數(shù)據(jù)。
簡單的說就是在某個(gè)東西已經(jīng)存在的情況下,我們還需要其他的一些附加信息來對它加以描述或者說對它進(jìn)行標(biāo)記,那么這些數(shù)據(jù)都可以稱為元數(shù)據(jù)。
3、java代碼為什么會需要元數(shù)據(jù)?java代碼有什么元數(shù)據(jù)?
至于它為什么需要元數(shù)據(jù),想要解答好這個(gè)問題的話,需要一些恰當(dāng)?shù)膱鼍皝磔o助描述才好解析。
例如拿類繼承的例子來說,子類在繼承父類時(shí),可以選擇重寫父類的方法,后續(xù)我們通過子類對象調(diào)用方法時(shí),需要調(diào)用的是跟父類方法名一樣的方法,但是因?yàn)樽宇愔貙懥诉壿嫞噪m然方法名相同,但是邏輯卻不一樣了;
在這種情況下,當(dāng)代碼在編譯時(shí),我們希望編譯器能夠幫助我們對這種繼承的代碼校驗(yàn)一下,看看這個(gè)方法是不是重寫的父類方法(檢查它的方法名、參數(shù)、返回值、權(quán)限修飾符等);
因?yàn)槲覀冊诙x一個(gè)子類繼承一個(gè)父類時(shí),子類也是可以定義自己的方法的,那編譯器是如何知道子類的哪些方法是需要進(jìn)行校驗(yàn)它是否是重寫的父類方法的呢?
這個(gè)時(shí)候應(yīng)該怎么辦?我們是不是得對重寫父類的方法標(biāo)記一下,到時(shí)候編譯階段好讓編譯器知道:噢,你就檢查那些做了標(biāo)記的方法是不是重寫的父類方法就行,沒做標(biāo)記的不用檢查了,它是子類自己定義的。
那這個(gè)標(biāo)記就是用于對代碼元素(Method)進(jìn)行描述的,它就是這個(gè)代碼元素的元數(shù)據(jù),而存儲這個(gè)標(biāo)記信息的就是注解;
java內(nèi)置了標(biāo)記方法是否重寫的注解,它就是@Override,這個(gè)注解我們應(yīng)該經(jīng)常能夠看到。
在編譯的時(shí)候,編譯器獲會判斷這個(gè)方法上是否使用了@Override這個(gè)注解,如果使用了這個(gè)注解,那么就檢查它是不是重寫的父類方法:主要是檢查父類中是不是也存在這么一個(gè)方法名、參數(shù)、返回值類型、權(quán)限修飾符等與它一樣的方法,如果是,則說明是重寫的父類方法,否則,它用了注解,但是父類中卻不存在這個(gè)方法,那就是代碼有問題,編譯就無法通過;如果方法沒有使用@Override這個(gè)注解,那編譯器就會認(rèn)為它是子類自己定義的方法,不是重寫的父類方法,那父類中不存在肯定是正常的。
也就是說我們強(qiáng)烈建議,開發(fā)人員在重寫父類方法時(shí)一定要加上@Override這個(gè)注解(現(xiàn)在idea這些開發(fā)工具可能都會自動加的),讓編譯器能夠?qū)@個(gè)方法進(jìn)行校驗(yàn),避免出現(xiàn)如下的問題:
我們分明是想重寫父類的saying()方法的,但是因?yàn)椴患?xì)心而把方法名寫錯(cuò)了,寫成了sayign(),而父類中又沒有toStrign()這個(gè)方法,并且因?yàn)殚_發(fā)者沒有加@Override注解,所以編譯器就認(rèn)為sayign()這個(gè)方法是子類自己定義的方法了,所以也不會報(bào)錯(cuò);
而這就會導(dǎo)致我們后期調(diào)用方法時(shí)可能會出現(xiàn)調(diào)用錯(cuò)的問題,很明顯,我們重寫方法,肯定是希望沿用父類定義的這個(gè)saying()方法的,只是實(shí)現(xiàn)邏輯不一樣而已,所以我們在重寫方法的時(shí)候,實(shí)際上實(shí)現(xiàn)的是sayign()的邏輯,但是我們潛意識里不知道自己把方法名寫錯(cuò)了,一直認(rèn)為自己修改的是saying()方法,所以最后調(diào)用的時(shí)候,調(diào)用的很大可能是saying()方法,但是這個(gè)方法依然還是父類的實(shí)現(xiàn)邏輯,沒有執(zhí)行到子類重寫的邏輯,根本不會達(dá)到我們的預(yù)期效果。
所以,這就是為什么我們強(qiáng)烈建議在重寫父類方法時(shí)為方法加上@Override注解,就是為了讓編譯器給我們的程序多加一層保障,有什么問題盡早在編譯階段就暴露出來(@Override標(biāo)記的重寫父類方法的檢查是在編譯階段執(zhí)行的),而不是等到程序跑起來了,在運(yùn)行期間再暴露出問題,這樣增加了排查難度甚至也增加修改的工作量,吃力不討好。
加了@Override注解,就是為了避免開發(fā)者們自己潛意識里也不知道方法名寫錯(cuò)了的問題,實(shí)際上自己以為的正在重寫的方法卻不是我們期望重寫的方法,方法名都不一樣了。
加個(gè)例子描述下上面的場景吧:
/**
* 父類
*/
public class Parent {
public void saying(){
System.out.println("我是父類");
}
}
/**
* 子類
*/
public class Child extends Parent {
public void sayign() {
System.out.println("我是子類");
}
public static void main(String[] args) {
new Child().saying();
}
}
我在子類中是想重寫父類的saying()方法的,但是卻寫錯(cuò)了名字,寫成了sayign(),又沒有加@Override注解,編譯器把它認(rèn)為是子類自己定義的了,編譯階段沒報(bào)錯(cuò),開發(fā)者也沒發(fā)現(xiàn)錯(cuò)了,到最后調(diào)用的時(shí)候new Child().saying(),果然調(diào)用了saying(),結(jié)果打出來的是:我是父類,但是我預(yù)期的結(jié)果應(yīng)該是:我是子類。所以這樣就造成結(jié)果跟預(yù)期不一致。

但是如果我加了@Override注解,因?yàn)楦割愔衧ayign()這個(gè)方法,既然父類沒有這個(gè)方法,自然重寫的說法也不成立,所以在編譯期會報(bào)錯(cuò):

所以java代碼某些情況下需要元數(shù)據(jù),這樣代碼的處理器才能夠?qū)Υa進(jìn)行正確的處理。
而代碼元素有什么元數(shù)據(jù),這個(gè)得根據(jù)實(shí)際的場景來確定,比如這個(gè)在編譯階段校驗(yàn)子類方法是否重寫了方法,就需要指定的方法包含【重寫了父類方法標(biāo)記】的元數(shù)據(jù)。
4、為什么要引入java注解呢?
如前面所說的,引入java注解的目的是為了用于存儲java代碼元素的元數(shù)據(jù)信息。
是的,這個(gè)說了那么多遍,相信大家都記住了,都了解了它就是用于存儲信息的,但是我們還得知道我們什么時(shí)候需要用注解來存儲這些元數(shù)據(jù)信息吧,注解存儲的元數(shù)據(jù)信息最終又會被誰使用?
一句話,注解最終的使用者是注解處理器,注解處理器就是獲取依附在代碼元素上的注解并解析出注解中存儲的信息的java工具(我下一章會寫一個(gè)注解處理器的案例協(xié)助理解,揭示注解處理器的面目)。
而什么時(shí)候會使用注解,總結(jié)下來大概有這3大類情況吧:
1、編譯器編譯代碼時(shí)需要對某些代碼進(jìn)行特殊檢查或特殊處理的,這種情況下需要對代碼做標(biāo)識,以變編譯器能夠識別哪些代碼是需要特殊檢查或處理的代碼是哪些,這時(shí)候就可以用注解來存儲這些標(biāo)記信息;
這類注解只會在源碼階段(.java文件)保留,編譯后的.class文件中不會存在這些注解的信息了。
2、在jvm加載java的.class文件到內(nèi)存中時(shí)可能需要對某些字節(jié)碼文件動態(tài)地做處理,如修改字節(jié)碼文件的內(nèi)容,這種場景下也需要標(biāo)記好哪些代碼元素是需要被修改的,可以通過注解來存儲這個(gè)標(biāo)記;
因?yàn)閖vm加載類信息是通過加載字節(jié)碼流的形式進(jìn)行的,所以注解處理器要獲取注解的信息的話,那么.class文件中必然得保留注解信息才行,也就是說這類注解在經(jīng)過編譯之后,它會存在于字節(jié)碼文件中。
3、在程序運(yùn)行期間,某些情況下我們可能也需要對代碼元素進(jìn)行標(biāo)記,舉個(gè)例子,大家應(yīng)該開發(fā)過數(shù)據(jù)庫處理相關(guān)的程序,就是crud那套流程,我們要操作數(shù)據(jù)庫的話,首先是不是得先建表,那我們在java代碼層面是不是需要定義一個(gè)與數(shù)據(jù)庫表對應(yīng)的java類,然后如果要全部在java代碼中實(shí)現(xiàn)表的創(chuàng)建、表的增刪改查操作的話,我們有什么方法可供選擇:
1)每一個(gè)javaBean類的建表、數(shù)據(jù)的增刪改查操作的sql語句都事先定義好,相當(dāng)于直接維護(hù)一個(gè)常量類,里面定義的sql語句表示javaBean類與數(shù)據(jù)庫表之間的關(guān)系,每一個(gè)操作的sql語句都事先定義好;
public class SqlMap {
private static final String createUserSql = "create table user(" +
"id int PRIMARY KEY AUTO_INCREMENT," +
"name varchar(255) not null," +
"age int not null)" +
"ENGINE=InnoDB DEFAULT CHARSET=utf8;";
/**
* ...
*/
}
2)另外的選擇就是我不想額外維護(hù)一個(gè)類來表示表的操作,希望通過注解的方式來為javaBean類的代碼元素添加信息,然后通過注解處理器解析注解并生成這些建表以及增刪改查語句等。
@Table("user")
public class User {
/**
* 比如通過注解存儲了描述類屬性的信息:
* 這個(gè)屬性在數(shù)據(jù)庫表中的對應(yīng)的字段類型是int,
* 再者這個(gè)屬性對應(yīng)的表字段的約束是PRIMARY KEY AUTO_INCREMENT,主鍵且id自增長;
* 這些信息都是對一個(gè)類的屬性進(jìn)行描述,后面通過注解處理器創(chuàng)建表時(shí)就可以從注解中知道這個(gè)類屬性對應(yīng)的表字段的名稱是什么,
* 類型是什么,對這個(gè)字段的約束又是什么等等。
*
*/
@Attribute("attr=id,type=int,constraint=PRIMARY KEY AUTO_INCREMENT")
private int id;
@Attribute("attr=name,type=varchar(255),constraint=NOT NULL")
private String name;
@Attribute("attr=age,type=int,constraint=NOT NULL")
private int age;
}
所以你看,我們通過注解標(biāo)注了類以及屬性的元數(shù)據(jù),后面我們就可以實(shí)現(xiàn)一個(gè)注解處理器來解析這些注解最后生成我們預(yù)期的sql語句,由于這是發(fā)生在程序運(yùn)行期間的,也就是說要在jvm運(yùn)行期間解析注解的信息的話,那么注解的信息必然是被加載到j(luò)vm的內(nèi)存中了。
為什么描述java代碼的元數(shù)據(jù)就要引入注解呢?不能用其他的方式嗎?
當(dāng)然不是非得使用注解,也還有其他的方式,如使用xml文件來描述代碼元素的元數(shù)據(jù),比如用過mybatis的人都知道,mybatis就是使用xml文件來存儲javaBean類與數(shù)據(jù)庫表的映射關(guān)系的,這個(gè)xml文件存儲的映射關(guān)系就是javaBean的元數(shù)據(jù)。
那為什么有xml這種方式還要引入注解呢?關(guān)于這個(gè)問題,我查閱了很多資料,眾說紛紜,一方覺得xml是單獨(dú)拎出來一個(gè)文件描述元數(shù)據(jù),不跟代碼摻雜在一塊xml,這是降低了代碼的耦合度,這是好事;另一方又有人覺得代碼的元數(shù)據(jù)直接體現(xiàn)在代碼上,這樣代碼的可讀性更好,更利于理解。
總之,我覺得吧,xml和注解沒有絕對的好壞,主要是看具體的應(yīng)用場景中選擇哪種開發(fā)起來更方便,后續(xù)維護(hù)起來更容易,那么這種技術(shù)在這種場景下就是最合適的。