前言:三年之前就買了《Java程式設計思想》這本書,但是到現在為止都還沒有好好看過這本書,這次希望能夠堅持通讀完整本書並整理好自己的讀書筆記,上一篇文章是記錄的第十七章到第十八章的內容,這一次記錄的是第十九章到第二十章的內容,相關範例程式碼放在碼雲上了,碼雲地址:https://gitee.com/reminis_com/thinking-in-java
關鍵字enum可以將一組具名的值的有限集合建立為一種新的型別,而這些具名的值可以作為常規的程式元件使用,這是一種非常有用的功能。
我們已經知道,呼叫enum的values()方法,可以遍歷enum範例。values()方法返回enum範例的陣列,而且該陣列中的元素嚴格保持其在enum中宣告時的順序,因此你可以在迴圈中使用values()返回的陣列。
建立enum時,編譯器會為你生成一個相關的類,這個類繼承自java.lang.Enum。下面的例子演示了Enum提供的一些功能∶
package enumerated;
/**
* @author Mr.Sun
* @date 2022年09月02日 15:58
*
* 列舉的基本特性
*/
public class EnumClass {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()) {
System.out.println(s + " ordinal: " + s.ordinal());
System.out.print(s.compareTo(Shrubbery.CRAWLING) + " ");
System.out.print(s.equals(Shrubbery.CRAWLING) + " ");
System.out.println(s == Shrubbery.CRAWLING);
System.out.println(s.getDeclaringClass());
System.out.println(s.name());
System.out.println("----------------------");
}
// 從字串名稱生成列舉值
for(String s : "HANGING CRAWLING GROUND".split(" ")) {
Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);
System.out.println(shrub);
}
}
}
enum Shrubbery {
GROUND, CRAWLING, HANGING
}
執行結果如下圖:
ordinal()方法返回一個int值,這是每個enum範例在宣告時的次序,從0開始。可以使用==來比較enum範例,編譯器會自動為你提供equals()和hashCode()方法。Enum類實現了Comparable 介面,所以它具有compareTo()方法。同時,它還實現了Serializable介面。
如果在enum範例上呼叫getDeclaringClass()方法,我們就能知道其所屬的enum類。name()方法返回enum範例宣告時的名字,這與使用toString()方法效果相同。valueOf()是在Enum中定義的static方法,它根據給定的名字返回相應的enum範例,如果不存在給定名字的範例,將會丟擲異常。
前面已經提到,編譯器為你建立的enum類都繼承自Enum類。然而,如果你研究一下Enum 類就會發現,它並沒有values()方法。可我們明明已經用過該方法了,難道存在某種「隱藏的」方法嗎?我們可以利用反射機制編寫一個簡單的程式,來檢視其中的究竟∶
package enumerated;
import utils.OSExecute;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;
/**
* @author Mr.Sun
* @date 2022年09月02日 16:10
*
* 使用反射機制研究列舉類的values()
*/
enum Explore {
HERE, THERE
}
public class Reflection {
public static Set<String> analyze(Class<?> enumClass) {
System.out.println("----- Analyzing " + enumClass + " -----");
System.out.println("Interfaces:");
for (Type t : enumClass.getGenericInterfaces()) {
System.out.println(t);
}
System.out.println("Base: " + enumClass.getSuperclass());
System.out.println("Methods: ");
Set<String> methods = new TreeSet<String>();
for (Method method : enumClass.getMethods()) {
methods.add(method.getName());
}
System.out.println(methods);
return methods;
}
public static void main(String[] args) {
Set<String> exploreMethods = analyze(Explore.class);
Set<String> enumMethods = analyze(Enum.class);
System.out.println("Explore.containsAll(Enum)? " + exploreMethods.containsAll(enumMethods));
System.out.print("Explore.removeAll(Enum): ");
exploreMethods.removeAll(enumMethods);
System.out.println(exploreMethods);
// Decompile the code for the enum:
OSExecute.command("javap G:/github/cnblogs/gitee/thinking-in-java/out/production/thinking-in-java/enumerated/Explore.class");
}
} /* Output:
----- Analyzing class enumerated.Explore -----
Interfaces:
Base: class java.lang.Enum
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, values, wait]
----- Analyzing class java.lang.Enum -----
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
Compiled from "Reflection.java"
final class enumerated.Explore extends java.lang.Enum<enumerated.Explore> {
public static final enumerated.Explore HERE;
public static final enumerated.Explore THERE;
public static enumerated.Explore[] values();
public static enumerated.Explore valueOf(java.lang.String);
static {};
}
*///:~
答案是,values()是由編譯器新增的static方法。可以看出,在建立Explore的過程中,編譯器還為其新增了valueOf()方法。這可能有點令人迷惑,Enum類不是已經有valueOf()方法了嗎。不過Enum中的valueOf()方法需要兩個引數,而這個新增的方法只需一個引數。由於這裡使用的Set只儲存方法的名字,而不考慮方法的簽名,所以在呼叫Explore.removeAl(Enum)之後,就只剩下【values】了。
從最後的輸出中可以看到,編譯器將Explore標記為final類,所以無法繼承自enum。其中還有一個static的初始化子句,稍後我們將學習如何重定義該句。
由於擦除效應(在第15章中介紹過),反編譯無法得到Enum的完整資訊,所以它展示的Explore的父類別只是一個原始的Enum,而非事實上的Enum
由於values()方法是由編譯器插入到enum定義中的static方法,所以,如果你將enum範例向上轉型為Enum,那麼values()方法就不可存取了。不過,在Class中有一個getEnumConstants()方法,所以即便Enum介面中沒有values()方法,我們仍然可以通過Class物件取得所有enum範例∶
enum Search {
HITHER, YON
}
public class UpcastEnum {
public static void main(String[] args) {
Search[] values = Search.values();
for (Search val : values) {
System.out.println(val.name());
}
System.out.println("--------------");
Enum e = Search.HITHER;
for (Enum en : e.getClass().getEnumConstants()) {
System.out.println(en);
}
}
}
執行結果如下:
Set是一種集合,只能向其中新增不重複的物件。當然,enum也要求其成員都是唯一的,所以enum看起來也具有集合的行為。不過,由於不能從enum中刪除或新增元素,所以它只能算是不太有用的集合。Java SE5引入EnumSet,是為了通過enum建立一種替代品,以替代傳統的"基於int的「位標誌」。這種標誌可以用來表示某種「開/關」資訊,不過,使用這種標誌,我們最終操作的只是一些bit,而不是這些bit想要表達的概念,因此很容易寫出令人難以理解的程式碼。
EnumSet的設計充分考慮到了速度因素,因為它必須與非常高效的bit標誌相競爭(其操作與HashSet相比,非常地快)。就其內部而言,它(可能)就是將一個long值作為位元向量,所以EnumSet非常快速高效。使用EnumSet的優點是,它在說明一個二進位制位是否存在時,具有更好的表達能力,並且無需擔心效能。EnumSet中的元素必須來自一個enum。下面的enum表示在一座大樓中,警報感測器的安放位置∶
package enumerated;
public enum AlarmPoints {
STAIR1, STAIR2,
LOBBY,
OFFICE1, OFFICE2, OFFICE3, OFFICE4,
BATHROOM, UTILITY, KITCHEN
}
然後,我們使用EnumSet來跟蹤報警器的狀態:
package enumerated;
import java.util.EnumSet;
import static enumerated.AlarmPoints.*;
/**
* @author Mr.Sun
* @date 2022年09月02日 17:18
*/
public class EnumSetTest {
public static void main(String[] args) {
EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class); // Empty set
points.add(BATHROOM);
System.out.println(points);
points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points = EnumSet.allOf(AlarmPoints.class);
points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
System.out.println(points);
points = EnumSet.complementOf(points);
System.out.println(points);
}
}
執行結果如下圖:
EnumMap是一種特殊的Map,它要求其中的鍵(key)必須來自一個enum。由於enum本身的限制,所以EnumMap在內部可由陣列實現。因此EnumMap的速度很快,我們可以放心地使用enum範例在EnumMap中進行查詢操作。不過,我們只能將enum的範例作為鍵來呼叫put()方法,其他操作與使用一般的Map差不多。
下面的例子演示了命令設計模式的用法。一般來說,命令模式首先需要一個只有單一方法的介面,然後從該介面實現具有各自不同的行為的多個子類。接下來,程式設計師就可以構造命令物件,並在需要的時候使用它們了∶
package enumerated;
import java.util.EnumMap;
import java.util.Map;
import static enumerated.AlarmPoints.*;
/**
* @author Mr.Sun
* @date 2022年09月02日 17:25
*
* 使用EnumMap
*/
interface Command{ void action(); }
public class EnumMapTest {
public static void main(String[] args) {
EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
em.put(KITCHEN, () -> System.out.println("Kitchen fire!"));
em.put(BATHROOM, () -> System.out.println("Bathroom alert!"));
for(Map.Entry<AlarmPoints,Command> e : em.entrySet()) {
System.out.print(e.getKey() + ": ");
e.getValue().action();
}
try {
// If there's no value for a particular key:
em.get(UTILITY).action();
} catch(Exception e) {
System.out.println(e);
}
}
}/* Output:
BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
java.lang.NullPointerException
*///:~
與EnumSet一樣,enum範例定義時的次序決定了其在EnumMap中的順序。
main()方法的 最後部分說明,enum的每個範例作為一個鍵,總是存在的,但是如果你沒有為這個鍵呼叫put()方法來存入相應的值的話,對應的值就是null。
Java的Enum有一個非常有趣的特性,即它允許程式設計師為enum範例編寫方法,從而為每個enum範例賦予各自不同的行為,要實現常數相關的方法,你需要為enum定義一個或多個abstract方法,然後為每個enum範例實現該抽象方法。參考下面的例子:
package enumerated;
import java.text.DateFormat;
import java.util.Date;
public enum ConstantSpecificMethod {
DATE_TIME {
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH {
String getInfo() {
return System.getenv("CLASSPATH");
}
},
VERSION {
String getInfo() {
return System.getProperty("java.version");
}
};
abstract String getInfo();
public static void main(String[] args) {
for(ConstantSpecificMethod csm : values()) {
System.out.println(csm.getInfo());
}
}
}/* Output:
2022-9-2
null
1.8.0_211
*///:~
在職責鏈(Chain of Responsibility)設計模式中,程式設計師以多種不同的方式來解決一個問題,然後將它們連結在一起。當一個請求到來時,它遍歷這個鏈,直到鏈中的某個解決方案能夠處理該請求。
通過常數相關的方法,我們可以很容易地實現一個簡單的職責鏈。我們以一個郵局的模型為例。郵局需要以儘可能通用的方式來處理每一封郵件,並且要不斷嘗試處理郵件,直到該郵件最終被確定為一封死信。其中的每一次嘗試可以看作為一個策略(也是一個設計模式),而完整的處理方式列表就是一個職責鏈。
我們先來描述一下郵件。郵件的每個關鍵特徵都可以用enum來表示。程式將隨機地生成Mail物件,如果要減小一封郵件的GeneralDelivery為YES的概率,那最簡單的方法就是多建立幾個不是YES的enum範例,所以enum的定義看起來有點古怪。
我們看到Mail中有一個randomMail()方法,它負責隨機地建立用於測試的郵件。而generator()方法生成一個Iterable物件,該物件在你呼叫next()方法時,在其內部使用randomMail()來建立Mail物件。這樣的結構使程式設計師可以通過呼叫Mail.generator()方法,很容易地構造出一個foreach迴圈∶
package enumerated;
import utils.Enums;
import java.util.Iterator;
/**
* @author Mr.Sun
* @date 2022年09月02日 17:45
*
* 以郵局的模型為例,通過常數相關的方法,實現一個簡單的職責鏈
*/
public class PostOffice {
enum MailHandler {
GENERAL_DELIVERY {
boolean handle(Mail m) {
switch (m.generalDelivery) {
case YES:
System.out.println("Using general delivery for " + m);
return true;
default:
return false;
}
}
},
MACHINE_SCAN {
boolean handle(Mail m) {
switch (m.scannability) {
case UNSCANNABLE:
return false;
default:
switch (m.address) {
case INCORRECT:
return false;
default:
System.out.println("Delivering " + m + " automatically");
return true;
}
}
}
},
VISUAL_INSPECTION {
boolean handle(Mail m) {
switch (m.readability) {
case ILLEGIBLE:
return false;
default:
switch (m.address) {
case INCORRECT:
return false;
default:
System.out.println("Delivering " + m + " normally");
return true;
}
}
}
},
RETURN_TO_SENDER {
boolean handle(Mail m) {
switch (m.returnAddress) {
case MISSING:
return false;
default:
System.out.println("Returning " + m + " to sender");
return true;
}
}
};
abstract boolean handle(Mail m);
}
static void handle(Mail m) {
for(MailHandler handler : MailHandler.values()) {
if(handler.handle(m)) {
return;
}
}
System.out.println(m + " is a dead letter");
}
public static void main(String[] args) {
for(Mail mail : Mail.generator(10)) {
System.out.println(mail.details());
handle(mail);
System.out.println("*****");
}
}
}
class Mail {
// 「否」會降低隨機選擇的概率:
enum GeneralDelivery {YES, NO1, NO2, NO3, NO4, NO5}
enum Scannability {UNSCANNABLE, YES1, YES2, YES3, YES4}
enum Readability {ILLEGIBLE, YES1, YES2, YES3, YES4}
enum Address {INCORRECT, OK1, OK2, OK3, OK4, OK5, OK6}
enum ReturnAddress {MISSING, OK1, OK2, OK3, OK4, OK5}
GeneralDelivery generalDelivery;
Scannability scannability;
Readability readability;
Address address;
ReturnAddress returnAddress;
static long counter = 0;
long id = counter++;
public String toString() { return "Mail " + id; }
public String details() {
return toString() +
", General Delivery: " + generalDelivery +
", Address Scanability: " + scannability +
", Address Readability: " + readability +
", Address Address: " + address +
", Return address: " + returnAddress;
}
/**
* 生成測試郵件
*/
public static Mail randomMail() {
Mail m = new Mail();
m.generalDelivery= Enums.random(GeneralDelivery.class);
m.scannability = Enums.random(Scannability.class);
m.readability = Enums.random(Readability.class);
m.address = Enums.random(Address.class);
m.returnAddress = Enums.random(ReturnAddress.class);
return m;
}
public static Iterable<Mail> generator(final int count) {
return new Iterable<Mail>() {
int n = count;
public Iterator<Mail> iterator() {
return new Iterator<Mail>() {
public boolean hasNext() { return n-- > 0; }
public Mail next() { return randomMail(); }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
}
/* Output:
Mail 0, General Delivery: NO2, Address Scanability: UNSCANNABLE, Address Readability: YES3, Address Address: OK1, Return address: OK1
Delivering Mail 0 normally
*****
Mail 1, General Delivery: NO5, Address Scanability: YES3, Address Readability: ILLEGIBLE, Address Address: OK5, Return address: OK1
Delivering Mail 1 automatically
*****
Mail 2, General Delivery: YES, Address Scanability: YES3, Address Readability: YES1, Address Address: OK1, Return address: OK5
Using general delivery for Mail 2
*****
Mail 3, General Delivery: NO4, Address Scanability: YES3, Address Readability: YES1, Address Address: INCORRECT, Return address: OK4
Returning Mail 3 to sender
*****
Mail 4, General Delivery: NO4, Address Scanability: UNSCANNABLE, Address Readability: YES1, Address Address: INCORRECT, Return address: OK2
Returning Mail 4 to sender
*****
Mail 5, General Delivery: NO3, Address Scanability: YES1, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK2
Delivering Mail 5 automatically
*****
Mail 6, General Delivery: YES, Address Scanability: YES4, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK4
Using general delivery for Mail 6
*****
Mail 7, General Delivery: YES, Address Scanability: YES3, Address Readability: YES4, Address Address: OK2, Return address: MISSING
Using general delivery for Mail 7
*****
Mail 8, General Delivery: NO3, Address Scanability: YES1, Address Readability: YES3, Address Address: INCORRECT, Return address: MISSING
Mail 8 is a dead letter
*****
Mail 9, General Delivery: NO1, Address Scanability: UNSCANNABLE, Address Readability: YES2, Address Address: OK1, Return address: OK4
Delivering Mail 9 normally
*****
*///:~
職責鏈由enum MailHandler實現,而enum定義的次序決定了各個解決策略在應用時的次序。對每一封郵件,都要按此順序嘗試每個解決策略,直到其中一個能夠成功地處理該郵件,如果所有的策略都失敗了,那麼該郵件將被判定為一封死信。
註解(也被稱為後設資料)為我們在程式碼中新增資訊提供了一種形式化的方法,使我們可以在稍後某個時刻非常方便地使用某些資料。
package annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
除了@符號以外,@Test的定義很像一個空的介面。定義註解時,會需要一些元註解(meta-annotation),如@Target和@Retention。@Target用來定義你的註解將應用於什麼地方(例如是一個方法或者一個域)。@Rectetion用來定義該註解在哪一個級別可用,在原始碼中(SOURCE)、類檔案中(CLASS)或者執行時(RUNTIME)。
沒有元素的註解稱為標記註解,例如上例種的@Test。
註解元素可用的型別如下:
如果你使用了其它型別,編譯器就會報錯。注意,也不允許使用任何包裝型別,不過由於自動打包的存在,這算不上什麼限制。註解也可以作為元素的型別,也就是說,註解可以巢狀。
Java目前只內建了四種元註解,元註解專職負責註解其它的註解:
大多數時候,程式設計師主要是定義自己的註解,並編寫自己的處理器來處理它們。
下面是一個簡單的註解,我們可以用它來跟蹤一個專案中的用例。如果一個方法或一組方法實現了某個用例的需求,那麼程式設計師可以為此方法加上該註解。於是,專案經理通過計算已經實現的用例,就可以很好地掌控專案的進展。而如果要更新或修改系統的業務邏輯,則維護該專案的開發人員也可以很容易地在程式碼中找到對應的用例。
package annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}
注意,id和description類似方法定義。由於編譯器會對id進行型別檢查,因此將用例檔案的追蹤資料庫與原始碼相關聯是可靠的。description元素有一個default值,如果在註解某個方法時沒有給出description的值,則該註解的處理器就會使用此元素的預設值。
在下面的類中,有三個方法被註解為用例∶
package annotations;
import java.util.List;
/**
* @author Mr.Sun
* @date 2022年09月03日 9:05
*
* 註解用例
*/
public class PasswordUtils {
@UseCase(id = 47, description = "密碼必須至少包含一個數位")
public boolean validatePassword(String password) {
return (password.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "新密碼不能等於以前使用的密碼")
public boolean checkForNewPassword(List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
註解的元素在使用時表現為名一值對的形式,並需要置於@UseCase宣告之後的括號內。在encryptPassword()方法的註解中,並沒有給出description元素的值,因此,在UseCase的註解處理器分析處理這個類時會使用該元素的預設值。
你應該能夠想象得到如何使用這套工具來「勾勒」出將要建造的系統,然後在建造的過程中逐漸實現系統的各項功能。
如果沒有用來讀取註解的工具,那註解也不會比註釋更有用。使用註解的過程中,很重要的一個部分就是建立與使用註解處理器。Java SE5擴充套件了反射機制的API,以幫助程式設計師構造這類工具。同時,它還提供了一個外部工具apt幫助程式設計師解析帶有註解的Java原始碼。
下面是一個非常簡單的註解處理器,我們將用它來讀取PasswordUtils類,並使用反射機制查詢@UseCase標記。我們為其提供了一組id值,然後它會列出在PasswordUtils種找到的用例,以及缺失的用例。
package annotations;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Mr.Sun
* @date 2022年09月03日 9:11
*
* 編寫註解處理器
*/
public class UseCaseTracker {
public static void trackUseCases(List<Integer> useCases, Class<?> clazz) {
for (Method m : clazz.getDeclaredMethods()) {
UseCase useCase = m.getAnnotation(UseCase.class);
if (useCase != null) {
System.out.println("找到@UseCase:" + useCase.id() + " " + useCase.description());
useCases.remove(Integer.valueOf(useCase.id()));
}
}
for (Integer i : useCases) {
System.out.println("警告: 缺失用例:" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
} /* Output:
找到@UseCase:49 新密碼不能等於以前使用的密碼
找到@UseCase:47 密碼必須至少包含一個數位
找到@UseCase:48 no description
警告: 缺失用例:50
*///:~
這個程式用到了兩個反射的方法∶getDeclaredMethods()和getAnnotation(),它們都屬於AnnotatedElement介面(Class、Method與Feld等類都實現了該介面)。getAnnoation()方法返回指定型別的註解物件,在這裡就是UseCase。如果被註解的方法上沒有該型別的註解,則返回null值。然後我們通過呼叫id()和description()方法從返回的UseCase物件中提取元素的值。其中,encriptPassword()方法在註解的時候沒有指定description的值,因此處理器在處理它對應的註解時,通過description()方法取得的是預設值no description。
列舉和註解其實在日常開發中都很熟悉,因為是非常基礎的知識,本文也只是把模糊的概念和比較冷門的知識點記錄下來,方便日後查閱,原來是打算把第二十一章的內容也放在本文的,但發現並行這章內容太多了,限於篇幅,還是打算單獨寫一篇文章進行記錄,而且並行也是比較重要的基礎知識。等把並行這章內容看完了,《Java程式設計思想》讀書筆記系列也就告一段落了。