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

2021-02-19 明日IT部落

自 JDK5 推出以來,註解已成為Java生態系統不可缺少的一部分。雖然開發者為Java框架(例如Spring的@Autowired)開發了無數的自定義註解,但編譯器認可的一些註解非常重要。

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

首先,我們將深入研究Java中最常用的註解之一:@Override。

@Override

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

public class Foo {
   public boolean equals(Foo foo) {
       // Check if the supplied object is equal to this object
   }
}

由於所有類都隱式地從Object類繼承,Foo類的目的是覆蓋Object#equals方法,因此Foo可被測試是否與Java中的任何其他對象相等。雖然我們的意圖是正確的,但我們的實現則並非如此。實際上,我們的實現根本不覆蓋Object#equals方法。相反,我們提供了方法的重載:我們不是替換Object類提供的equals方法的實現,而是提供第二個方法來專門接受Foo對象,而不是Object對象。我們的錯誤可以用簡單實現來舉例說明,該實現對所有的相等檢查都返回true,但當提供的對象被視為Object(Java將執行的操作,例如在Java Collections Framework即JCF中)時,就永遠不會調用它:

public class Foo {
   public boolean equals(Foo foo) {
       return true;
   }
}
Object foo = new Foo();
Object identicalFoo = new Foo();
System.out.println(foo.equals(identicalFoo));    // false

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

public class Foo {
   @Override
   public boolean equals(Foo foo) {
       return true;
}
}

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

$ javac Foo.java
Foo.java:3: error: method does not override or implement a method from a supertype
       @Override
       ^
1 error

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

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

public abstract class Foo {
   public int doSomething() {
       return 1;
   }
   public abstract int doSomethingElse();
}
public class Bar extends Foo {
   @Override
   public int doSomething() {
       return 10;
   }
   @Override
   public int doSomethingElse() {
       return 20;
   }
}
Foo bar = new Bar();
System.out.println(bar.doSomething());         // 10
System.out.println(bar.doSomethingElse());     // 20

@Override註解不僅不限於超類中的具體或抽象方法,而且還可用於確保接口的方法也被覆蓋(從JDK 6開始):

public interface Foo {
   public int doSomething();
}
public class Bar implements Foo {
   @Override
   public int doSomething() {
       return 10;
   }
}
Foo bar = new Bar();
System.out.println(bar.doSomething());    // 10

通常,覆蓋非final類方法、抽象超類方法或接口方法的任何方法都可以使用@Override進行註解。有關有效覆蓋的更多信息,請參閱《Overriding and Hiding》文檔 以及《Java Language Specification (JLS)》的第9.6.4.4章節。

@FunctionalInterface

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

一個函數式接口只有一個抽象方法。由於默認方法有一個實現,所以它們不是抽象的。

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

public interface Foo {
   public int doSomething();
}
public interface Bar {
   public int doSomething();
   public default int doSomethingElse() {
       return 1;
   }
}

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

public class FunctionalConsumer {
   public void consumeFoo(Foo foo) {
       System.out.println(foo.doSomething());
   }
   public void consumeBar(Bar bar) {
       System.out.println(bar.doSomething());
   }
}
FunctionalConsumer consumer = new FunctionalConsumer();
consumer.consumeFoo(() -> 10);    // 10
consumer.consumeBar(() -> 20);    // 20

重點要注意的是,抽象類,即使它們只包含一個抽象方法,也不是函數式接口。更多信息,請參閱首席Java語言架構師Brian Goetz編寫的《Allow lambdas to implement abstract classes》。與@Override註解類似,Java編譯器提供了@FunctionalInterface註解以確保接口確實是函數式接口。例如,我們可以將此註解添加到上面創建的接口中:

@FunctionalInterface
public interface Foo {
   public int doSomething();
}
@FunctionalInterface
public interface Bar {
   public int doSomething();
   public default int doSomethingElse() {
       return 1;
   }
}

如果我們錯誤地將接口定義為非函數接口並用@FunctionalInterface註解了錯誤的接口,則Java編譯器會發出錯誤。例如,我們可以定義以下帶註解的非函數式接口:

@FunctionalInterface
public interface Foo {
   public int doSomething();
   public int doSomethingElse();
}

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

$ javac Foo.java
Foo.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
 Foo is not a functional interface
   multiple non-overriding abstract methods found in interface Foo
1 error

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

有關@FunctionalInterface註解的更多信息,請參閱@FunctionalInterface文檔和《JLS》的第4.6.4.9章節。

@SuppressWarnings

警告是所有編譯器的重要組成部分,為開發人員提供的反饋——可能危險的行為或在未來的編譯器版本中可能會出現的錯誤。例如,在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註解與抑制警告(以字符串數組的形式提供)的一個或多個名字添加到發生警告的上下文中:

public class Foo {
   public void doSomething(@SuppressWarnings("rawtypes") List myList) {
       // Do something with myList
   }
}

@SuppressWarnings註解可用於以下任何一種情況:

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

public class Foo {
   public void doSomething() {
       @SuppressWarnings("rawtypes")
       List myList = new ArrayList();
       // Do something with myList
   }
}

@SafeVarargs

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

List ln = new ArrayList<Number>();
ln.add(1);
List<String> ls = ln;                 // unchecked warning
String s = ls.get(0);                 // ClassCastException

在將ln分配給ls後,堆中存在變量ls,該變量具有List<String>的類型,但存儲引用到實際為List<Number>類型的值。這個無效的引用被稱為堆汙染。由於直到運行時才能確定此錯誤,因此它會在編譯時顯示為警告,並在運行時出現ClassCastException。當泛型參數與可變參數組合時,可能會加劇此問題:

public class Foo {
   public <T> void doSomething(T... args) {
       // ...
   }
}

在這種情況下,Java編譯器會在調用站點內部創建一個數組來存儲可變數量的參數,但是T的類型並未實現,因此在運行時會丟失。實質上,到doSomething的參數實際上是Object[]類型。如果依賴T的運行時類型,那麼這會導致嚴重的問題,如下面的代碼片段所示:

public class Foo {
   public <T> void doSomething(T... args) {
       Object[] objects = args;
       String string = (String) objects[0];
   }
}
Foo foo = new Foo();
foo.<Number>doSomething(1, 2);

如果執行此代碼片段,那麼將導致ClassCastException,因為在調用站點傳遞的第一個Number參數不能轉換為String(類似於獨立堆汙染示例中拋出的ClassCastException)。通常,可能會出現以下情況:編譯器沒有足夠的信息來正確確定通用可變參數的確切類型,這會導致堆汙染,這種汙染可以通過允許內部可變參數數組從方法中轉義來傳播,如下面摘自《Effective Java》第3版 pp.147的例子:

public static <T> T[] toArray(T... args) {
   return args;
}

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

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

因此,如果我們創建了以下方法(來自pp.149同上),那麼我們可以用@SafeVarags註解來合理地註解我們的方法:

@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
   List<T> result = new ArrayList<>();
   for (List<? extends T> list : lists) {
       result.addAll(list);
   }
   return result;
}

有關@SafeVarargs註解的更多信息,請參閱@SafeVarargs文檔,《JLS》9.6.4.7章節以及《Effective Java》第3版中的Item32。

@Deprecated

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

為了支持不推薦使用的代碼的文檔,Java包含@Deprecated註解,它會將一些構造函數、域、局部變量、方法、軟體包、模塊、參數或類型標記為已棄用。如果棄用的元素(構造函數,域,局部變量等)被使用了,則編譯器發出警告。例如,我們可以創建一個棄用的類並按如下所示使用它:

@Deprecated
public class Foo {}
Foo foo = new Foo();

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

$ javac Main.java
Note: Main.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

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

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

聲明被註解禁止棄用警告(即@SuppressWarnings(「deprecation」)註解,如上所述,應用於使用棄用元素的上下文。

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

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

exports或opens指令內。

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

@Deprecated(forRemoval = true)
public class Foo {}

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

$ javac Main.java
Main.java:7: warning: [removal] Foo in com.foo has been deprecated and marked for removal
               Foo foo = new Foo();
               ^
Main.java:7: warning: [removal] Foo in com.foo has been deprecated and marked for removal
               Foo foo = new Foo();
                             ^
2 warnings

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

@Deprecated(since = "1.0.5", forRemoval = true)
public class Foo {}

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

/**
* Some test class.
*
* @deprecated Replaced by {@link com.foo.NewerFoo}.
*
* @author Justin Albano
*/
@Deprecated(since = "1.0.5", forRemoval = true)
public class Foo {}

JavaDoc工具將生成以下文檔:

有關@Deprecated註解的更多信息,請參閱@Deprecated文檔和《JLS》9.6.4.6章節。

結尾

自JDK 5引入註解以來,註解一直是Java不可缺少的一部分。雖然有些註解比其他註解更受歡迎,但本文中介紹的這5種註解是新手級別以上的開發人員都應該理解和掌握的:@Override,@FunctionalInterface,@SuppressWarnings,@SafeVarargs,和@Deprecated。雖然每種方法都有其獨特的用途,但所有這些註解使得Java應用程式更具可讀性,並允許編譯器對我們的代碼執行一些其他隱含的假設。隨著Java語言的不斷發展,這些經過實踐驗證的註解可能服務多年,幫助確保更多的應用程式按開發人員的意圖行事。

相關焦點

  • 每個Java開發者都應該知道的5個JDK工具
    目前,有許許多多的JDK工具呈現在大家面前,但最常用的莫過於java.exe、javac.exe、jar等。除了這幾個,還有哪些呢?本文作者Joe擁有多年的Java開發經驗,其在博客上分享了一篇文章:5 JDK Tools Every Java Developer Should Know,筆者對其進行了編譯,以下為譯文。
  • Java 程式設計師必須掌握的 5 個註解!
    自 JDK5 推出以來,註解已成為Java生態系統不可缺少的一部分。雖然開發者為Java框架(例如Spring的@Autowired)開發了無數的自定義註解,但編譯器認可的一些註解非常重要。在本文中,我們將看到5個Java編譯器支持的註解,並了解其期望用途。順便,我們將探索其創建背後的基本原理,圍繞其用途的一些特質,以及正確應用的一些例子。雖然其中有些註解比其他註解更為常見,但非初學Java開發人員都應該消化了解每個註解。
  • Java註解Annotation與自定義註解詳解
    Junit測試:@TestSpring的一些註解:@Controller、@RequestMapping、@RequestParam、@ResponseBody、@Service、@Component、@Repository、@Resource、@AutowireJava驗證的註解:@NotNull、@Email下面看一下注解Override.java
  • Java中的註解是如何工作的?
    J2SE5.0版本在 java.lang.annotation提供了四種元註解,專門註解其他的註解:@Documented –註解是否將包含在JavaDoc中@Retention –什麼時候使用該註解@Target?
  • Java中的註解到底是如何工作的?
    J2SE5.0版本在 java.lang.annotation提供了四種元註解,專門註解其他的註解:@Documented –註解是否將包含在JavaDoc中@Retention –什麼時候使用該註解@Target?
  • 原來 java 註解只是個標記,沒什麼本領,一文精通,值得收藏!
    這回我知道這個標記的作用了,它可以告訴編譯器和閱讀源碼的人這個方法是覆蓋或實現超類型的方法。@Retention 註解中的成員變量(value)用來設置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚舉類型,RetentionPolicy 有 3 個枚舉常量,如下所示。
  • 一起來學 Java 註解 (Annotation)
    Annotation的作用給某個類、方法…添加了一個註解,這個環節僅僅是做了一個標記,對代碼本身並不會造成任何影響,需要後續環節的配合,需要其他方法對該註解賦予業務邏輯處理。就如同我們在微信上發了一個共享定位,此時並沒有什麼用,只有當後面其他人都進入了這個共享定位,大家之間的距離才能明確,才知道該怎麼聚在一起。
  • 學生會私房菜【20201125】《Java註解》
    別急,我們從最熟悉的陌生人@Override開始,最熟悉是因為我們知道這是方法重寫,子類覆蓋父類方法用到的註解,陌生是因為我們從來沒有點進去了解過這個註解,那接下來就進去看看吧! import java.lang.annotation.
  • Java反射註解妙用,學會事半功倍--文末送書
    主函數完整的主函數代碼前言最近在做項目權限,使用shiro實現restful接口權限管理,對整個項目都進行了重構。這裡使用了標準的restful接口風格,swagger自動API接口,shiro 接口權限註解@RequiresPermissions組合成的一個controller。當然也可以使用其他技術,只要能獲取到接口信息就行。註解不重要,重要的是註解裡的信息。
  • Java中註解與反射的使用方法及場景,強行解釋一波!
    註解註解定義Java 註解(Annotation)又稱 Java 標註,是 JDK5.0 引入的一種注釋機制。Java 語言中的類、方法、變量、參數和包等都可以被標註。和注釋不同,Java 標註可以通過反射獲取標註內容。在編譯器生成類文件時,標註可以被嵌入到字節碼中。
  • 【都是乾貨】這20+種常用註解,學Java的必須掌握!
    註解和 xml 都是一種元數據,元數據即解釋數據的數據,這就是所謂配置。>@Autowired:由Spring提供@Inject:由JSR-330提供@Resource:由JSR-250提供都可以註解在set方法和屬性上,推薦註解在屬性上(一目了然,少寫代碼)。
  • 不會 Java 註解 ? 看這一篇文章!
    於是我們查,大概知道,哦,這是個註解,有了這個註解,UserBean就會被裝載進Spring容器中,我們可以知道這些信息,但是,它實際上做了哪些事情,是如何起作用,也就是如何把UserBean交給Spring去處理的,這個內部實現,我們不清楚。但是,我們雖然不清楚,有個東西它清楚,什麼呢?
  • Java必須掌握的12種Spring常用註解!
    層)@Repository 在數據訪問層使用(dao層)@Controller 在展現層使用,控制器的聲明(C)2.注入bean的註解@Autowired:由Spring提供@Inject:由JSR-330提供@Resource:由JSR-250提供都可以註解在set方法和屬性上,推薦註解在屬性上(一目了然
  • 這一篇 Java 註解,寫得太好了!
    什麼是註解註解其實就是一種標記,常常用於代替冗餘複雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的Override、Deprecated等,但是它本身並不起任何作用,可以說有它沒它都不影響程序的正常運行,註解的作用在於「註解的處理程序」,註解處理程序通過捕獲被註解標記的代碼然後進行一些處理
  • 關於Java註解,你不得不看的一篇文章
    什麼是註解註解其實就是一種標記,常常用於代替冗餘複雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的Override、Deprecated等,但是它本身並不起任何作用,可以說有它沒它都不影響程序的正常運行,註解的作用在於「註解的處理程序」,註解處理程序通過捕獲被註解標記的代碼然後進行一些處理
  • java中註解的使用
    說在前面使用註解開發的好處1. 使代碼更加乾淨易讀,易於維護修改。比如,以前使用spring的開發,都是基於xml文件實現了統一的配置管理,但是缺點也是顯而易見的,就是隨著項目的越來越大,xml文件會變得越來越複雜,維護成本也會越來越高。使用註解就可以提供更大的便捷性,易於維護修改。2.
  • 【都給你總結好了!】你必須掌握的 21 個 Java 核心技術!
    不過對於一個有著3年以上Java經驗的資深開發者來說,不會JVM幾乎是不可接受的。JVM作為java運行的基礎,很難相信對於JVM一點都不了解的人可以把java語言吃得很透。我在面試有超過3年Java經驗的開發者的時候, JVM幾乎就是一個必問的問題了。當然JVM不是唯一決定技術能力好壞的面試問題,但是可以佐證java開發能力的高低。
  • Java開發必須掌握的 20+ 種 Spring 常用註解
    註解和xml都是一種元數據,元數據即解釋數據的數據,這就是所謂配置。本文主要羅列Spring|SpringMVC相關註解的簡介。:由JSR-330提供@Resource:由JSR-250提供都可以註解在set方法和屬性上,推薦註解在屬性上(一目了然,少寫代碼)。
  • 關於每個版本特性的Java 面試題
    舉個例子:在 JDK 1.5 之前,當我們要為一個傳遞多個類型相同的參數時,我們有兩種方法解決,1.直接傳遞一個數組過去,2.有多少個參數就傳遞多少個參數。個靜態的常量表示類的實例;3)jdk5 中新定義了枚舉類型,專門用於解決此類問題;4)枚舉就是一個特殊的java類,可以定義屬性、方法、構造函數、實現接口、繼承類;為什麼要有枚舉?
  • @lombok註解背後的原理是什麼,讓我們走近自定義Java註解處理器
    點擊藍色「程式設計師黃小斜」關注我喲加個「星標」,每天和你一起多進步一點點!本文介紹了如何自定義Java註解處理器及涉及到的相關知識,看完本文可以很輕鬆看懂並理解各大開源框架的註解處理器的應用。《遊園不值》 應憐屐齒印蒼苔 ,小扣柴扉久不開 。春色滿園關不住 ,一枝紅杏出牆來 。