前言:三年之前就買了《Java編程思想》這本書,但是到現在爲止都還沒有好好看過這本書,這次希望能夠堅持通讀完整本書並整理好自己的讀書筆記,上一篇文章是記錄的第十七章到第十八章的內容,這一次記錄的是第十九章到第二十章的內容,相關示例代碼放在碼雲上了,碼雲地址:https://gitee.com/reminis_com/thinking-in-java
第十九章:枚舉類型
關鍵字enum可以將一組具名的值的有限集合創建爲一種新的類型,而這些具名的值可以作爲常規的程序組件使用,這是一種非常有用的功能。
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實例,如果不存在給定名字的實例,將會拋出異常。
values()的神祕之處
前面已經提到,編譯器爲你創建的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);
}
}
}
運行結果如下:
使用EnumSet代替標誌
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
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
*///:~
使用enum的職責鏈
在職責鏈(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。
註解元素
註解元素可用的類型如下:
- 所有基本類型(int, float, boolean等)
- String
- Class
- enum
- Annotation
- 以上類型的數組
如果你使用了其它類型,編譯器就會報錯。注意,也不允許使用任何包裝類型,不過由於自動打包的存在,這算不上什麼限制。註解也可以作爲元素的類型,也就是說,註解可以嵌套。
元註解
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編程思想》讀書筆記系列也就告一段落了。