[Java] 使用EnumSet代替位運算簡化程式碼邏輯
- 2020 年 1 月 20 日
- 筆記
位運算
在Review程式碼時候,看到一段涉及到USB的邏輯程式碼,他是這樣寫的
private boolean isUsbConnected; private boolean isUsbModeNCM; private boolean isUsbModeAccessory; private boolean isUsbModeAdb; private boolean isUsbModeMTP; ...
然後程式碼邏輯里是大量的成員變數的判斷,顯得非常臃腫而且難讀懂,大量的if-else判斷讓程式碼邏輯很脆弱,稍微一個情況沒考慮好就會出現難以排查的bug。
所以這種情況使用位掩碼進行處理會更簡單:
// 部落格地址:wossoneri.github.io private static final int FLAG_USB_CONNECTED = 0x1; private static final int FLAG_USB_MODE_NCM = 0x1 << 1; private static final int FLAG_USB_MODE_ACY = 0x1 << 2; private static final int FLAG_USB_MODE_ADB = 0x1 << 3; private static final int FLAG_USB_MODE_MTP = 0x1 << 4; ... private int mUsbState; public void addUsbState(int flag) { mUsbState |= flag; } public void removeUsbState(int flag) { mUsbState &= ~flag; } public boolean isUsbStateEnable(int flag) { return (mUsbState & flag) == flag; }簡單分析一下這樣寫的好處:
FLAG_USB_CONNECTED = 0001 FLAG_USB_MODE_NCM = 0010 FLAG_USB_MODE_ACY = 0100 FLAG_USB_MODE_ADB = 1000
通過移位,使得每一位都有獨立的代表的意義,1代表enable,0代表disable。
如果要添加狀態(Java里int值默認賦值為0):
public void addUsbState(int flag) { mUsbState |= flag; }假設添加accessory狀態FLAG_USB_MODE_ACY
0000 |= 0100 -> 0100
所以mUsbState就是0100的狀態了。
繼續添加FLAG_USB_MODE_ADB狀態
0100 |= 1000 -> 1100
也可以一次添加多個狀態,比如上面的兩個狀態在一次設置同時添加:
addUsbState(FLAG_USB_MODE_ACY | FLAG_USB_MODE_ADB);
結果就是:
0000 |= (0100 | 1000) -> 0000 |= 1100 -> 1100
如果是原來的boolean變數,就需要單獨為每一個變數設置,就會很麻煩。
然後是移除狀態:
public void removeUsbState(int flag) { mUsbState &= ~flag; }比如接著上面移除FLAG_USB_MODE_ADB狀態
1100 &= ~1000 -> 1100 &= 0111 -> 0100
如果移除一個不存在的狀態比如FLAG_USB_MODE_NCM
0100 &= ~0010 -> 0100 &= 1101 -> 0100
可以看到並不會對當前狀態造成任何影響。
最後看一下檢查狀態:
public boolean isUsbStateEnable(int flag) { return (mUsbState & flag) == flag; }首先檢查一下當前擁有的狀態:
(0100 & 0100) == 0100 -> 0100 == 0100 -> true
可以檢測到該狀態。然後換一個狀態:
(0100 & 1000) == 1000 -> 0000 == 1000 -> false
沒有檢測到該狀態。
所以,通過三個簡單的方法,就可以檢查一個變數里保存的所有狀態,避免了使用大量bool變數進行挨個檢查。簡化了程式碼,增加程式碼可讀性,並且使程式碼更加穩定。
進階!使用EnumSet替代位運算
到這裡你可能覺得問題解決了就完了,但是還沒有!
實際上,《Effective Java》這本書有對位域的一項討論:
位域表示法也允許利用位操作,有效的執行像union和intersection這樣的集合操作。但位域有著int枚舉常量所有的缺點,甚至更多。當位域以數字形式列印時,翻譯位域比翻譯簡單的int枚舉常量要困難很多。甚至要遍歷位域表示的所有元素也沒有很容易的方法。
Java.util包提供了EnumSet類來有效地表示從單個枚舉類型中提取的多個值的多個集合。這個類實現Set介面,提供豐富的功能、類型安全性以及可從其他Set實現中得到的互用性。
內部實現上,每個EnumSet內容都表示為位矢量,一般(低於64個元素)整個EnumSet就是用一個long的位運算來表示的。也就是說它替你使用位演算法實現了這一切,避免你自己寫位運算導致程式碼難讀懂的情況。
下面是用EnumSet修改後的示例程式碼,它更加簡短,清楚也更安全。
// 部落格地址:wossoneri.github.io public class UsbManager { private EnumSet<UsbFlags> mUsbState = EnumSet.noneOf(UsbFlags.class); public enum UsbFlags { CONNECTED, NCM, ACCESSORY, ADB, MTP } public void addFlag(UsbFlags flag) { mUsbState.add(flag); System.out.println("After add flag " + flag + ", Now state is " + this.mUsbState); } public void addFlag(Set<UsbFlags> flags) { mUsbState.addAll(flags); System.out.println("After add flags " + flags + ", Now state is " + this.mUsbState); } public void removeFlag(UsbFlags flag) { mUsbState.remove(flag); System.out.println("After remove flag " + flag + ", Now state is " + this.mUsbState); } public void removeFlag(Set<UsbFlags> flags) { mUsbState.removeAll(flags); System.out.println("After remove flags " + flags + ", Now state is " + this.mUsbState); } public boolean checkFlagEnabled(UsbFlags flag) { return mUsbState.contains(flag); } public boolean checkFlagEnabled(Set<UsbFlags> flag) { return mUsbState.containsAll(flag); } public void printUsbState() { System.out.println("Current usb state is " + mUsbState); } }測試用例以及輸出
public static void main(String[] args) { // 部落格地址:wossoneri.github.io UsbManager usbManager = new UsbManager(); usbManager.printUsbState(); // 添加一項flag usbManager.addFlag(UsbFlags.CONNECTED); // 添加一組 flag usbManager.addFlag(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB)); // 檢查存在的一個flag System.out.println("mUsbState contains flag " + UsbFlags.ACCESSORY + ": " + usbManager.checkFlagEnabled(UsbFlags.ACCESSORY)); // 檢查不存在的一個flag System.out.println("mUsbState contains flag " + UsbFlags.MTP + ": " + usbManager.checkFlagEnabled(UsbFlags.MTP)); // 檢查一組存在的flag System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB) + ": " + usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB))); // 檢查一組包含不存在的flag System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.MTP) + ": " + usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.MTP))); // 檢查一組都不存在的flag System.out.println("mUsbState contains flag " + EnumSet.of(UsbFlags.NCM, UsbFlags.MTP) + ": " + usbManager.checkFlagEnabled(EnumSet.of(UsbFlags.NCM, UsbFlags.MTP))); usbManager.printUsbState(); // 刪除一個不存在的flag usbManager.removeFlag(UsbFlags.MTP); // 刪除一個存在的flag usbManager.removeFlag(UsbFlags.ACCESSORY); // 刪除一組都不存在的flag usbManager.removeFlag(EnumSet.of(UsbFlags.NCM, UsbFlags.MTP)); // 刪除一組包含不存在的flag usbManager.removeFlag(EnumSet.of(UsbFlags.NCM, UsbFlags.ADB)); usbManager.addFlag(EnumSet.of(UsbFlags.ACCESSORY, UsbFlags.ADB)); // 刪除一組存在的flag usbManager.removeFlag(EnumSet.of(UsbFlags.ADB, UsbFlags.ACCESSORY)); }輸出為
Current usb state is [] After add flag CONNECTED, Now state is [CONNECTED] After add flags [ACCESSORY, ADB], Now state is [CONNECTED, ACCESSORY, ADB] mUsbState contains flag ACCESSORY: true mUsbState contains flag MTP: false mUsbState contains flag [ACCESSORY, ADB]: true mUsbState contains flag [ACCESSORY, MTP]: false mUsbState contains flag [NCM, MTP]: false Current usb state is [CONNECTED, ACCESSORY, ADB] After remove flag MTP, Now state is [CONNECTED, ACCESSORY, ADB] After remove flag ACCESSORY, Now state is [CONNECTED, ADB] After remove flags [NCM, MTP], Now state is [CONNECTED, ADB] After remove flags [NCM, ADB], Now state is [CONNECTED] After add flags [ACCESSORY, ADB], Now state is [CONNECTED, ACCESSORY, ADB] After remove flags [ACCESSORY, ADB], Now state is [CONNECTED]
綜上,程式碼唯一要注意的是
public boolean checkFlagEnabled(Set<UsbFlags> flag)
傳入參數使用了Set介面,這是考慮到可能會傳入其他Set的實現類型,所以傳入介面參數要好於實現類型參數。
最後,EnumSet類集成了位域自身的簡潔性和性能優勢,又擁有枚舉的所有優點,所以使用它代替位域是非常好的選擇。
