從 JDK5 開始,Java 開始引入註釋功能,今後,註釋已成爲許多 Java 應用程序和框架的重要組成部分。 在絕大多數狀況下,註釋將被用於描述語言結構,例如類,字段,方法等,可是在另外一種狀況下,能夠將註釋做爲可實現的接口。java
在常規的使用方法中,註釋就是註釋,接口就是接口。例如,下面的代碼爲接口 MyInterface 添加了一個註釋。編程
@Deprecated interface MyInterface { }
而接口也只能起到接口的做用,以下面的代碼,Person 實現了 IPerson 接口,並實現了 getName 方法。數組
interface IPerson { public String getName(); } class Person implements IPerson { @Override public String getName() { return "Foo"; } }
若是按註釋方式使用,那麼就是註釋,若是按接口方式使用,那麼就是接口。例如,下面的代碼定義了一個 Test 註釋。app
@Retention(RetentionPolicy.RUNTIME) @interface Test { String name(); }
Test 註釋經過 Retention 註釋進行修飾。Retention 註釋能夠用來修飾其餘註釋,因此稱爲元註釋,後面的 RetentionPolicy.RUNTIME 參數表示註釋不只被保存到 class 文件中,jvm 加載 class 文件以後,仍然存在。這樣在程序運行後,仍然能夠動態獲取註釋的信息。框架
Test 自己是一個註釋,有一個名爲 name 的方法,name 是一個抽象方法,須要在使用註釋時指定具體的值,其實 name 至關於 Test 的屬性。下面的 Sporter 類使用 Test 註釋修改了 run 方法。jvm
class Sporter { @Test(name = "Bill") public void run (){ } }
能夠經過反射獲取修飾 run 方法的註釋信息,例如,name 屬性的值,代碼以下:編程語言
Sporter sporter = new Sporter(); var annotation = sporter.getClass().getMethod("run").getAnnotations()[0]; var method = annotation.annotationType().getMethod("name"); System.out.println(method.invoke(annotation)); // 輸出 Bill
因爲 Test 中有 name 方法,因此乾脆就利用一下這個 name 方法,直接用類實現它,免得再定義一個相似的接口。代碼以下:ide
class Teacher implements Test { @Override public String name() { return "Mike"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } }
要注意的是,若是要實現一個註釋,那麼必須實現 annotationType 方法,該方法返回了註釋的類型,這裏返回了 Test 的 Class 對象。儘管大多數狀況下,都不須要實現一個註釋,不過在一些狀況,如註釋驅動的框架內,可能會頗有用。this
在 Java 中,與大多數面向對象編程語言同樣,可使用構造方法實例化對象,固然,也有一些例外,例如,Java 對象的反序列化就不須要經過構造方法實例化對象(咱們先不去考慮這些例外)。還有一些實例化對象的方式從表面上看沒有使用構造方法,但本質上仍然使用了構造方法。spa
例如,經過靜態工廠模式來實例化對象,實際上是將類自己的構造方法聲明爲 private,這樣就不能直接經過類的構造方法實例化對象了,而必須經過類自己的方法來調用這個被聲明爲 private 的構造方法來實例化對象,因而就有了下面的代碼:
class Person { private final String name; private Person(String name) { this.name = name; } public String getName() { return name; } // 靜態工廠方法 public static Person withName(String name) { return new Person(name); } } public class InitDemo { public static void main(String[] args){ // 經過靜態工廠方法實例化對象 Person person = Person.withName("Bill"); System.out.println(person.getName()); } }
所以,當咱們但願初始化一個對象時,咱們將初始化邏輯放到對象的構造方法中。 例如,咱們在 Person 類的構造方法中經過參數 name 初始化了 name 成員變量。 儘管彷佛能夠合理地假設全部初始化邏輯都在類的一個或多個構造方法中找到。但對於 Java,狀況並不是如此。在 Java 中,除了能夠在構造方法中初始化對象外,還能夠經過代碼塊來初始化對象。
class Car { // 普通的代碼塊 { System.out.println("這是在代碼塊中輸出的"); } public Car() { System.out.println("這是在構造方法中輸出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); } }
經過在類的內部定義一堆花括號來完成初始化邏輯,這就是代碼塊的做用,也能夠將代碼塊稱爲初始化器。實例化對象時,首先會調用類的初始化器,而後調用類的構造方法。 要注意的是,能夠在類中指定多個初始化器,在這種狀況下,每一個初始化器將按着定義的順序調用。
class Car { // 普通的代碼塊 { System.out.println("這是在第 1 個代碼塊中輸出的"); } // 普通的代碼塊 { System.out.println("這是在第 2 個代碼塊中輸出的"); } public Car() { System.out.println("這是在構造方法中輸出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); } }
除了普通的代碼塊(初始化器)外,咱們還能夠建立靜態代碼塊(也稱爲靜態初始化器),這些靜態初始化器在將類加載到內存時執行。 要建立靜態初始化器,咱們只需在普通初始化器前面加 static 關鍵字便可。
class Car { { System.out.println("這是在普通代碼塊中輸出的"); } static { System.out.println("這是在靜態代碼塊中輸出的"); } public Car() { System.out.println("這是在構造方法中輸出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); new Car(); } }
靜態初始化器只執行一次,並且是最早執行的代碼塊。例如,上面的代碼中,建立了兩個 Car 對象,但靜態塊只會執行一次,並且是最早執行的,普通代碼塊和 Car 類的構造方法,在每次建立 Car 實例時都會依次執行。
若是隻是代碼塊或構造方法,並不複雜,但若是構造方法、普通代碼塊和靜態代碼塊同時出如今類中時就稍微複雜點,在這種狀況下,會先執行靜態代碼塊,而後執行普通代碼塊,最後才執行構造方法。當引入父類時,狀況會變得更復雜。父類和子類的靜態代碼塊、普通代碼塊和構造方法的執行規則以下:
按聲明順序執行父類中全部的靜態代碼塊
按聲明順序執行子類中全部的靜態代碼塊
按聲明順序執行父類中全部的普通代碼塊
執行父類的構造方法
按聲明順序執行子類中全部的普通代碼塊
執行子類的構造方法
下面的代碼演示了這一執行過程:
class Car { { System.out.println("這是在 Car 普通代碼塊中輸出的"); } static { System.out.println("這是在 Car 靜態代碼塊中輸出的"); } public Car() { System.out.println("這是在 Car 構造方法中輸出的"); } } class MyCar extends Car { { System.out.println("這是在 MyCar 普通代碼塊中輸出的"); } static { System.out.println("這是在 MyCar 靜態代碼塊中輸出的"); } public MyCar() { System.out.println("這是在 MyCar 構造方法中輸出的"); } } public class InitDemo { public static void main(String[] args){ new MyCar(); } }
許多編程語言都包含某種語法機制,可使用很是少的代碼快速建立列表(數組)和映射(字典)對象。 例如,C ++可使用大括號初始化,這使開發人員能夠快速建立枚舉值列表,甚至在對象的構造方法支持此功能的狀況下初始化整個對象。 不幸的是,在 JDK 9 以前,所以,在 JDK9 以前,咱們仍然須要痛苦而無奈地使用下面的代碼建立和初始化列表:
List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3);
儘管上面的代碼能夠很好完成咱們的目標:建立包含 3 個整數值的 ArrayList 對象。但代碼過於冗長,這要求開發人員每次都要使用變量(myInts)的名字。爲了簡化這段 diamante,可使用雙括號來完成一樣的工做。
List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }};
雙花括號初始化其實是多個語法元素的組合。首先,咱們建立一個擴展 ArrayList 類的匿名內部類。 因爲 ArrayList 沒有抽象方法,所以咱們能夠爲匿名類實現建立一個空的實體。
List<Integer> myInts = new ArrayList<>() {};
使用這行代碼,實際上建立了原始 ArrayList 徹底相同的 ArrayList 匿名子類。他們的主要區別之一是咱們的內部類對包含的類有隱式引用,咱們正在建立一個非靜態內部類。 這使咱們可以編寫一些有趣的邏輯(若是不是很複雜的話),例如將捕獲的此變量添加到匿名的,雙花括號初始化的內部類代碼以下:
package black.magic; import java.util.ArrayList; import java.util.List; class InitDemo { public List<InitDemo> getListWithMeIncluded() { return new ArrayList<InitDemo>() {{ add(InitDemo.this); }}; } } public class DoubleBraceInitialization { public static void main(String[] args) { List<Integer> myInts2 = new ArrayList<>() {}; InitDemo demo = new InitDemo(); List<InitDemo> initList = demo.getListWithMeIncluded(); System.out.println(demo.equals(initList.get(0))); } }
若是上面代碼中的內部類是靜態定義的,則咱們將沒法訪問 InitDemo.this。 例如,如下代碼靜態建立了名爲 MyArrayList 的內部類,但沒法訪問 InitDemo.this 引用,所以不可編譯:
class InitDemo { public List<InitDemo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<InitDemo> {{ add(InitDemo.this); // 這裏會編譯出錯 }} }
從新建立雙花括號初始化的 ArrayList 的構造以後,一旦咱們建立了非靜態內部類,就可使用實例初始化(如上所述)來在實例化匿名內部類時執行三個初始元素的加法。 因爲匿名內部類會當即實例化,而且匿名內部類中只有一個對象存在,所以咱們實質上建立了一個非靜態內部單例對象,該對象在建立時會添加三個初始元素。 若是咱們分開兩個大括號,這將變得更加明顯,其中一個大括號清楚地構成了匿名內部類的定義,另外一個大括號表示了實例初始化邏輯的開始:
List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } };
儘管該技巧頗有用,但 JDK 9(JEP 269)已用一組 List(以及許多其餘收集類型)的靜態工廠方法代替了此技巧的實用程序。 例如,咱們可使用這些靜態工廠方法建立上面的列表,代碼以下:
List<Integer> myInts = List.of(1, 2, 3);
之因此須要這種靜態工廠技術,主要有兩個緣由:
(1)不須要建立匿名內部類;
(2)減小了建立列表所需的樣板代碼(噪音)。
不過以這種方式建立列表的代價是:列表是隻讀的。也就是說一旦建立後就不能修改。 爲了建立可讀寫的列表,就只能使用前面介紹的雙花括號初始化方式或者傳統的初始化方式了。
請注意,傳統初始化,雙花括號初始化和 JDK 9 靜態工廠方法不只可用於 List。 它們也可用於 Set 和 Map 對象,如如下代碼段所示:
Map<String, Integer> myMap1= new HashMap<>(); myMap1.put("key1", 10); myMap1.put("key2", 15); Map<String, Integer> myMap2 = new HashMap<>() {{ put("Key1", 10); put("Key2", 15); }}; Map<String, Integer> myMap3 = Map.of("key1", 10, "key2", 15);
在使用雙花括號方式初始化以前,要考慮它的性質,雖然確實提升了代碼的可讀性,但它帶有一些隱式的反作用。例如,會建立隱式對象。
註釋幾乎是每一個程序必不可少的組成部分,註釋的主要好處是它們不被執行,並且容易讓程序變得更可讀。 當咱們在程序中註釋掉一行代碼時,這一點變得更加明顯。咱們但願將代碼保留在咱們的應用程序中,但咱們不但願它被執行。 例如,如下程序致使將 5 打印到標準輸出:
public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); }
儘管不執行註釋是一個基本的假設,但這並非徹底正確的。 例如,如下代碼片斷會將什麼打印到標準輸出呢?
public static void main(String args[]) { int value = 5; // \u000dvalue = 8; System.out.println(value); }
你們必定猜想是 5,可是若是運行上面的代碼,咱們看到在 Console 中輸出了 8。 這個看似錯誤的背後緣由是 Unicode 字符\ u000d。 此字符其實是 Unicode 回車,而且 Java 源代碼由編譯器做爲 Unicode 格式的文本文件使用。 添加此回車符會將「value= 8;」換到註釋的下一行(在這一行沒有註釋,至關於在 value 前面按一下回車鍵),以確保執行該賦值。 這意味着以上代碼段實際上等於如下代碼段:
public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); }
儘管這彷佛是 Java 中的錯誤,但其實是該語言中的內置的功能。 Java 的最初目標是建立獨立於平臺的語言(所以建立 Java 虛擬機或 JVM),而且源代碼的互操做性是此目標的關鍵。 容許 Java 源代碼包含 Unicode 字符,這就意味着能夠經過這種方式包含非拉丁字符。 這樣能夠確保在世界一個區域中編寫的代碼(其中可能包含非拉丁字符,例如在註釋中)能夠在其餘任何地方執行。 有關更多信息,請參見 Java 語言規範或 JLS 的 3.3 節。 下面是一個更復雜的案例,你們看看輸出什麼東東。
package black.magic; public class ExecutableComments { public static void main(String args[]) { int value = 5; // \u000d\u0069\u006e\u0074\u0020\u0041value = 20; // A(65)、i(105)、n(110)、t(116) 空格:32 System.out.println(Avalue); } }
與 Java 中的類相比,枚舉的侷限性之一是枚舉不能從另外一個類或枚舉繼承。 例如,沒法執行如下操做:
public class Speaker { public void speak() { System.out.println("Hi"); } } public enum Person extends Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } } Person.JOE.speak();
可是,我可讓枚舉實現一個接口,併爲其抽象方法提供一個實現,以下所示:
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
如今,咱們還能夠在須要 Speaker 對象的任何地方使用 Person 的實例。 此外,咱們還能夠在每一個常量的基礎上提供接口抽象方法的實現(稱爲特定於常量的方法):
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph") { public void speak() { System.out.println("Hi, my name is Joseph"); } }, JIM("James"){ public void speak() { System.out.println("Hey, what's up?"); } }; private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
與本文中的其餘一些不一樣,應在適當的地方鼓勵使用此技術。 例如,若是可使用枚舉常量(例如 JOE 或 JIM)代替接口類型(例如 Speaker),則定義該常量的枚舉應實現接口類型。
在本文中,咱們研究了 Java 中的五個隱藏祕密:
(1)可擴展的註釋;
(2)實例初始化可用於在實例化時配置對象;
(3)用於初始化的雙花括號;
(4)可執行的註釋;
(5)枚舉能夠實現接口; 儘管其中一些功能有其適當的用途,但應避免使用其中某些功能(即建立可執行註釋)。 在決定使用這些機密時,請確保真的有必要這樣作。