[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類集成了位域自身的簡潔性和性能優勢,又擁有枚舉的所有優點,所以使用它代替位域是非常好的選擇。

Tags: