[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类集成了位域自身的简洁性和性能优势,又拥有枚举的所有优点,所以使用它代替位域是非常好的选择。