天天看點

每個 Java 開發者都應該知道的 5 個注解!

微信公衆号:javafirst

自 jdk5 推出以來,注解已成為java生态系統不可缺少的一部分。雖然開發者為java架構(例如spring的@autowired)開發了無數的自定義注解,但編譯器認可的一些注解非常重要。

在本文中,我們将看到5個java編譯器支援的注解,并了解其期望用途。順便,我們将探索其建立背後的基本原理,圍繞其用途的一些特質,以及正确應用的一些例子。雖然其中有些注解比其他注解更為常見,但非初學java開發人員都應該消化了解每個注解。

首先,我們将深入研究java中最常用的注解之一:@override。

每個 Java 開發者都應該知道的 5 個注解!

覆寫方法的實作或為抽象方法提供實作的能力是任何面向對象(oo)語言的核心。由于java是oo語言,具有許多常見的面向對象的抽象機制,是以在非終極超類定義的非最終方法或接口中的任何方法(接口方法不能是最終的)都可以被子類覆寫。雖然開始時覆寫方法看起來很簡單,但是如果執行不正确,則可能會引入許多微小的bug。例如,用覆寫類類型的單個參數覆寫object#equals方法就是一種常見的錯誤:

由于所有類都隐式地從object類繼承,foo類的目的是覆寫object#equals方法,是以foo可被測試是否與java中的任何其他對象相等。雖然我們的意圖是正确的,但我們的實作則并非如此。實際上,我們的實作根本不覆寫object#equals方法。相反,我們提供了方法的重載:我們不是替換object類提供的equals方法的實作,而是提供第二個方法來專門接受foo對象,而不是object對象。我們的錯誤可以用簡單實作來舉例說明,該實作對所有的相等檢查都傳回true,但當提供的對象被視為object(java将執行的操作,例如在java collections framework即jcf中)時,就永遠不會調用它:

這是一個非常微妙但常見的錯誤,可以被編譯器捕獲。我們的意圖是覆寫object#equals方法,但因為我們指定了一個類型為foo而不是object類型的參數,是以我們實際上提供了重載的object#equals方法,而不是覆寫它。為了捕獲這種錯誤,我們引入@override注解,它訓示編譯器檢查覆寫實際有沒有執行。如果沒有執行有效的覆寫,則會抛出錯誤。是以,我們可以更新foo類,如下所示:

如果我們嘗試編譯這個類,我們現在收到以下錯誤:

實質上,我們已經将我們已經覆寫方法的這一隐含的假設轉變為由編譯器進行的顯性驗證。如果我們的意圖被錯誤地實作,那麼java編譯器會發出一個錯誤——不允許我們不正确實作的代碼被成功編譯。通常,如果以下任一條件不滿足,則java編譯器将針對使用@override注解的方法發出錯誤(引用自override注解文檔):

該方法确實會覆寫或實作在超類中聲明的方法。

該方法的簽名與在object中聲明的任何公共方法(即equals或hashcode方法)的簽名覆寫等價(override-equivalent)。

是以,我們也可以使用此注解來確定子類方法實際上也覆寫超類中的非最終具體方法或抽象方法:

@override注解不僅不限于超類中的具體或抽象方法,而且還可用于確定接口的方法也被覆寫(從jdk 6開始):

通常,覆寫非final類方法、抽象超類方法或接口方法的任何方法都可以使用@override進行注解。有關有效覆寫的更多資訊,請參閱《overriding and hiding》文檔 以及《java language specification (jls)》的第9.6.4.4章節。

随着jdk 8中lambda表達式的引入,函數式接口在java中變得越來越流行。這些特殊類型的接口可以用lambda表達式、方法引用或構造函數引用代替。根據@functionalinterface文檔,函數式接口的定義如下:

一個函數式接口隻有一個抽象方法。由于預設方法有一個實作,是以它們不是抽象的。

例如,以下接口被視為函數式接口:

是以,下面的每一個都可以用lambda表達式代替,如下所示:

重點要注意的是,抽象類,即使它們隻包含一個抽象方法,也不是函數式接口。更多資訊,請參閱首席java語言架構師brian goetz編寫的《allow lambdas to implement abstract classes》。與@override注解類似,java編譯器提供了@functionalinterface注解以確定接口确實是函數式接口。例如,我們可以将此注解添加到上面建立的接口中:

如果我們錯誤地将接口定義為非函數接口并用@functionalinterface注解了錯誤的接口,則java編譯器會發出錯誤。例如,我們可以定義以下帶注解的非函數式接口:

如果我們試圖編譯這個接口,則會收到以下錯誤:

使用這個注解,我們可以確定我們不會錯誤地建立原本打算用作函數式接口的非函數式接口。需要注意的是,即使在@functionalinterface注解不存在的情況下,接口也可以用作函數式接口(可以替代為lambdas,方法引用和構造函數引用),正如我們前面的示例中所見的那樣。這類似于@override注解,即一個方法是可以被覆寫的,即使它不包含@override注解。在這兩種情況下,注解都是允許編譯器執行期望意圖的可選技術。

有關@functionalinterface注解的更多資訊,請參閱@functionalinterface文檔和《jls》的第4.6.4.9章節。

警告是所有編譯器的重要組成部分,為開發人員提供的回報——可能危險的行為或在未來的編譯器版本中可能會出現的錯誤。例如,在java中使用泛型類型而沒有其關聯的正式泛型參數(稱為原始類型)會導緻警告,就像使用不推薦使用的代碼一樣(請參閱下面的@deprecated部分)。雖然這些警告很重要,但它們可能并不總是适用甚至并不總是正确的。例如,可能會有對不安全的類型轉換發生警告的情況,但是基于使用它的上下文,我們可以保證它是安全的。

為了忽略某些上下文中的特定警告,jdk 5中引入了@suppresswarnings注解。此注解接受一個或多個字元串參數——描述要忽略的警告名稱。雖然這些警告的名稱通常在編譯器實作之間有所不同,但有3種警告在java語言中是标準化的(是以在所有java編譯器實作中都很常見):

unchecked:表示類型轉換未經檢查的警告(編譯器無法保證類型轉換是安全的),導緻發生的可能原因有通路原始類型的成員(參見《jls》4.8章節)、窄參考轉換或不安全的向下轉換(參見《jls》5.1.6章節)、未經檢查的類型轉換(參見《jls》5.1.9章節)、使用帶有可變參數的泛型參數(參見《jls》8.4.1章節和下面的@safevarargs部分)、使用無效的協變傳回類型(參見《jls》8.4.8.3章節)、不确定的參數評估(參見《jls》15.12.4.2章節),未經檢查的方法引用類型的轉換(參見《jls》15.13.2章節)、或未經檢查的lambda類型的對話(參見《jls》15.27.3章節)。

deprecation:表示使用了已棄用的方法、類、類型等的警告(參見《jls》9.6.4.6章節和下面的@deprecated部分)。

removal:表示使用了最終廢棄的方法、類、類型等的警告(參見《jls》9.6.4.6章節和下面的@deprecated部分)。

為了忽略特定的警告,可以将@suppressedwarning注解與抑制警告(以字元串數組的形式提供)的一個或多個名字添加到發生警告的上下文中:

@suppresswarnings注解可用于以下任何一種情況:

類型

方法

參數

構造函數

局部變量

子產品

一般來說,@suppresswarnings注解應該應用于最直接的警告範圍。例如,如果方法中的局部變量應忽略警告,則應将@suppresswarnings注解應用于局部變量,而不是包含局部變量的方法或類:

可變參數在java中是一種很有用的技術手段,但在與泛型參數一起使用時,它們也可能會導緻一些嚴重的問題。由于泛型在java中是非特定的,是以具有泛型類型的變量的實際(實作)類型不能在運作時被斷定。由于無法做出此判斷,是以變量可能會存儲非其實際類型的引用到類型,如以下代碼片段所示(摘自《java generics faqs》):

在将ln配置設定給ls後,堆中存在變量ls,該變量具有list<string>的類型,但存儲引用到實際為list<number>類型的值。這個無效的引用被稱為堆污染。由于直到運作時才能确定此錯誤,是以它會在編譯時顯示為警告,并在運作時出現classcastexception。當泛型參數與可變參數組合時,可能會加劇此問題:

在這種情況下,java編譯器會在調用站點内部建立一個數組來存儲可變數量的參數,但是t的類型并未實作,是以在運作時會丢失。實質上,到dosomething的參數實際上是object[]類型。如果依賴t的運作時類型,那麼這會導緻嚴重的問題,如下面的代碼片段所示:

如果執行此代碼片段,那麼将導緻classcastexception,因為在調用站點傳遞的第一個number參數不能轉換為string(類似于獨立堆污染示例中抛出的classcastexception)。通常,可能會出現以下情況:編譯器沒有足夠的資訊來正确确定通用可變參數的确切類型,這會導緻堆污染,這種污染可以通過允許内部可變參數數組從方法中轉義來傳播,如下面摘自《effective java》第3版 pp.147的例子:

在某些情況下,我們知道方法實際上是類型安全的,不會造成堆污染。如果可以在保證的情況下做出這個決定,那麼我們可以使用@safevarargs注解來注解該方法,進而抑制與可能的堆污染相關的警告。但是,這引出了一個問題:什麼時候通用可變參數方法會被認為是類型安全的?josh bloch在《effective java》第3版第147頁的基礎上提供了一個完善的解決方案——基于方法與内部建立的用于存儲其可變參數的數組的互動:

如果方法沒有存儲任何東西到數組(這會覆寫參數)且不允許對數組的引用進行轉義(這會使得不受信任的代碼可以通路數組),那麼它是安全的。換句話說,如果可變參數數組僅用于從調用者向方法傳遞可變數量的參數——畢竟,這是可變參數的目的——那麼該方法是安全的。

是以,如果我們建立了以下方法(來自pp.149同上),那麼我們可以用@safevarags注解來合理地注解我們的方法:

有關@safevarargs注解的更多資訊,請參閱@safevarargs文檔,《jls》9.6.4.7章節以及《effective java》第3版中的item32。

在開發代碼時,有時候代碼會變得過時和不應該再被使用。在這些情況下,通常會有個替補的更适合手頭的任務,且雖然現存的對過時代碼的調用可能會保留,但是所有新的調用都應該使用替換方法。這個過時的代碼被稱為不推薦使用的代碼。在某些緊急情況下,不建議使用的代碼可能會被删除,應該在未來的架構或庫版本從其代碼庫中删除棄用的代碼之前立即轉換為替換代碼。

為了支援不推薦使用的代碼的文檔,java包含@deprecated注解,它會将一些構造函數、域、局部變量、方法、軟體包、子產品、參數或類型标記為已棄用。如果棄用的元素(構造函數,域,局部變量等)被使用了,則編譯器發出警告。例如,我們可以建立一個棄用的類并按如下所示使用它:

如果我們編譯此代碼(在命名為main.java的檔案中),我們會收到以下警告:

通常,每當使用@deprecated注解的元素時,都會引發警告,除了用于以下五種情況:

聲明本身就被聲明為是棄用的(即遞歸調用)。

聲明被注解禁止棄用警告(即@suppresswarnings(“deprecation”)注解,如上所述,應用于使用棄用元素的上下文。

使用和聲明都在同一個最外面的類中(即,如果類調用其本身的棄用方法)。

用在import聲明中,該聲明導入通常不贊成使用的類型或構件(即,在将已棄用的類導入另一個類時)。

exports或opens指令内。

正如前面所說的,在某些情況下,當不推薦使用的元素将被删除,則調用代碼應立即删除不推薦使用的元素(稱為terminally deprecated code)。在這種情況下,可以使用forremoval參數提供的@deprecated注解,如下所示:

使用此最終棄用代碼會導緻一系列更嚴格的警告:

除了标準@deprcated注解所描述的相同異常之外,總是會發出最終棄用的警告。我們還可以通過為注解提供since變量來添加文檔到@deprecated注解中:

可以使用@deprecated javadoc元素(注意小寫字母d)進一步文檔化已棄用的元素,如以下代碼片段所示:

javadoc工具将生成以下文檔:

每個 Java 開發者都應該知道的 5 個注解!

有關@deprecated注解的更多資訊,請參閱@deprecated文檔和《jls》9.6.4.6章節。

自jdk 5引入注解以來,注解一直是java不可缺少的一部分。雖然有些注解比其他注解更受歡迎,但本文中介紹的這5種注解是新手級别以上的開發人員都應該了解和掌握的:@override,@functionalinterface,@suppresswarnings,@safevarargs,和@deprecated。雖然每種方法都有其獨特的用途,但所有這些注解使得java應用程式更具可讀性,并允許編譯器對我們的代碼執行一些其他隐含的假設。随着java語言的不斷發展,這些經過實踐驗證的注解可能服務多年,幫助確定更多的應用程式按開發人員的意圖行事。

每個 Java 開發者都應該知道的 5 個注解!