從 Bit 位操作學封裝

對於業務開發人員,bit 位這個概念既熟悉又陌生,熟悉是因爲整個計算機就是建立在 bit 基礎之上,同時任何一門語言都對 bit 位提供了支持;陌生是因爲工作過程中基本沒有使用過,說起具體的操作語法估計也需要好好思考一下。

不像底層研發人員,比如驅動開發、網絡協議開發等,成天與 bit 混在一起。業務開發人員對於 bit 位就陌生了太多。但,在一些特殊場景,bit 位還真的是一個非常好的解決方案。

1. 業務人員眼中的 bit 位

在業務開發人員眼裏,bit 位就是特殊的 一對多 關係,可以通過一個數值來表達多個 boolean 語義!

既然大家對 bit 不太熟悉,那就請出封裝利器,自己動手打造一套面向對象且更好用的工具。

2. 單一 Bit 位操作

對於單一 Bit 位,操作非常簡單,只有:

  1. 設置 bit 位爲 1(true)

  2. 設置 bit 位爲 0 (false)

  3. 驗證 bit 位 是否爲 1

操作代碼如下:

@Data
public class LongMaskOp{
    private final long mask;

    public LongMaskOp(long mask ) {
        this.mask = mask;
    }

    public boolean isSet(long code){
        return (code & this.mask) == this.mask;
    }

    public boolean match(long value) {
        return isSet(value);
    }

    public long set(long value, boolean isSet){
        if (isSet){
            return set(value);
        }else {
            return unset(value);
        }
    }

    public long set(long value){
        return value | this.mask;
    }

    public long unset(long value){
        return value & ~this.mask;
    }
}

使用也非常簡單,獲取 LongMaskOp 之後便可以使用各種方法進行位操作,示例如下:

long value = ?;
LongMaskOp longMaskOp = ?;

value = longMaskOp.set(value);
Assertions.assertTrue(longMaskOp.isSet(value));

value = longMaskOp.unset(value);
Assertions.assertFalse(longMaskOp.isSet(value));

代碼非常簡單,在此不在贅述,有了 LongMaskOp 類之後,如何獲取對應的實例呢?

當然,可以通過 new 關鍵字直接實例化對象,但每個位上的 MaskOp 是完全一樣的,這就讓我們想到了單例模式。

對於 Long 類型最多也就 64 個對象,我們可以一次性將其創建出來,然後通過傳入的參數返回對應位置上的值,具體操作步驟如下:

  1. 將 LongMaskOp 構造函數設置爲 private,禁止從外部創建對象

  2. LongMaskOp 中定義 64 個 靜態變量,每一位對於一個 LongMaskOp 實例

  3. 將 64 個 LongMaskOp 實例統一放在 List 集合,方便按位數進行查找

  4. 提供靜態方法,根據傳入的位數返回對應的 LongMaskOp 實例

核心代碼如下:

@Data
public class LongMaskOp{
    // 1. 私有構造函數,禁止從外部創建對象
    private LongMaskOp(long mask ) {
        this.mask = mask;
    }
    // 2. 定義 64 個靜態變量
    public static final LongMaskOp MASK_1 = new LongMaskOp(1L << 0);
    public static final LongMaskOp MASK_2 = new LongMaskOp(1L << 1);
    ...... 省略部分代碼
    public static final LongMaskOp MASK_64 = new LongMaskOp(1<< 63);
    // 3. 將 64 個變量放入 List 集合
    MASK_OPS = Arrays.asList(LongMaskOp.MASK_1, LongMaskOp.MASK_2, .... 省略部分代碼, LongMaskOp.MASK_64);
    // 4. 提供靜態方法獲取對應位置的 LongMaskOp 對象
    /**
     * 根據 bit 位的位置,獲取對應的封裝實例 <br />
     * Index 從 1 開始
     * @param index
     * @return
     */
    public static LongMaskOp getByBitIndex(int index){
        if (index < 1 || index > MASK_OPS.size()){
            throw new IndexOutOfBoundsException();
        }
        return MASK_OPS.get(index - 1);
    }
}

單例模式改造完成,通過以下代碼可以方便的獲取 Bit 操作類:

LongMaskOp bitOp1 = LongMaskOp.getByBitIndex(1);

整體結構如圖所示:

3. Bit 位邏輯運算

Bit 位支持 and、or、not 等邏輯運算,我們的 LongMaskOp 也需要提供對應的支持,可以思考一下,哪種模式可以解決這個問題?

對,這就是組合模式的應用場景。要想應用組合模式,需要:

  1. 抽取公共接口

  2. 構建葉子節點

  3. 構建組合節點

目前,LongMaskOp 實現了真正的業務邏輯,是我們的葉子節點。我們需要抽取公共接口,然後爲每個邏輯運算構建 “組合節點”。

首先,抽取公共接口,接口中只有一個 match 方法,用於判斷是否匹配,LongMaskOp 實現該接口並實現 match 方法:

public interface LongBitOp {
    boolean match(long value);
}
@Data
public class LongMaskOp implements LongBitOp {
    @Override
    public boolean match(long value) {
        return isSet(value);
    }
}

接下來,可以構建對 And、Or、Not 的支持。核心代碼如下:

public class LongBitAndOp implements LongBitOp {
    private final LongBitOp[] longBitOps;

    LongBitAndOp(LongBitOp... longBitOps) {
        this.longBitOps = longBitOps;
    }

    @Override
    public boolean match(long value) {
        if (this.longBitOps == null || this.longBitOps.length == 0){
            return true;
        }
        return Stream.of(longBitOps)
                .allMatch(longMaskOp -> longMaskOp.match(value));
    }
}

public class LongBitOrOp implements LongBitOp {
    private final LongBitOp[] longBitOps;

    LongBitOrOp(LongBitOp... longBitOps) {
        this.longBitOps = longBitOps;
    }

    @Override
    public boolean match(long value) {
        if (this.longBitOps == null || this.longBitOps.length == 0){
            return true;
        }
        return Stream.of(this.longBitOps)
                .anyMatch(intBitOp -> intBitOp.match(value));
    }
}

public class LongBitNotOp implements LongBitOp {
    private final LongBitOp longBitOp;

    LongBitNotOp(LongBitOp longBitOp) {
        this.longBitOp = longBitOp;
    }

    @Override
    public boolean match(long value) {
        return !this.longBitOp.match(value);
    }
}

邏輯非常簡單:

  1. LongBitAndOp 只有在所有的條件全部匹配時才返回 true

  2. LongBitOrOp 只要有一個條件匹配便返回 true

  3. LongBitNotOp 將條件判斷結果取反並返回

萬事具備,如何更好的暴露 API 呢?

首先,And、Or、Not 操作每個 LongBitOp 都應該支持,那放在 LongBitOp 接口最爲合適,放在接口中所有的子類都需要實現,哪豈不是更麻煩?

Java 8 引入一個新特性:默認方法,可以爲接口添加默認實現,一起瞧瞧:

public interface LongBitOp {
    boolean match(long value);

    // 默認方法
    default LongBitOp or(LongBitOp other){
        return new LongBitOrOp(this, other);
    }
    // 默認方法
    default LongBitOp and(LongBitOp other){
        return new LongBitAndOp(this, other);
    }
    // 默認方法
    default LongBitOp not(){
        return new LongBitNotOp(this);
    }
}

這樣每個 LongBitOp 子類都具備了 And、Or、Not 能力,新 API 使用起來也非常簡單:

LongMaskOp bitOp1 = LongMaskOp.getByBitIndex(1);
LongMaskOp bitOp2 = LongMaskOp.getByBitIndex(2);

LongBitOp orBitOp = bitOp1.or(bitOp2);
LongBitOp andBitOp = bitOp1.and(bitOp2);
LongBitOp notBitOp = bitOp1.not();

4. 數據庫過濾

Bit 位操作僅停留在 內存 嗎?不一定哈,MySQL 的 sql 語句便提供了 bit 位操作,可以在數據庫層對數據進行過濾。接下來,繼續對 LongBitOp 進行擴展,使其能產生語義相同的 sql 片段,輔助我們完成數據過濾。

首先,在 LongBitOp 接口中增加生成 SQL 片段的方法:

public interface LongBitOp {
    String toSqlFilter(String fieldName);
}

接下來,讓 LongMaskOp 實現 toSqlFilter 方案:

@Data
public class LongMaskOp implements LongBitOp {
    @Override
    public String toSqlFilter(String fieldName) {
        return new StringBuilder()
                .append("(")
                .append(fieldName)
                .append(" & ")
                .append(getMask())
                .append(")")
                .append("=")
                .append(getMask())
                .toString();
    }
}

沒什麼複雜的,主要就是 字符串 拼接,下一步對 LongBitAndOp、LongBitOrOp、LongBitNotOp 進行完善:

public class LongBitAndOp implements LongBitOp {
    // 使用 and 連接多個語句
    @Override
    public String toSqlFilter(String fieldName) {
        if (this.longBitOps == null || this.longBitOps.length == 0){
            return "";
        }
        return Stream.of(longBitOps)
                .map(intBitOp -> intBitOp.toSqlFilter(fieldName))
                .collect(Collectors.joining(" and ","(",")"));
    }
}

public class LongBitOrOp implements LongBitOp {
      // 使用 or 連接多個語句
    @Override
    public String toSqlFilter(String fieldName) {
        if (this.longBitOps == null || this.longBitOps.length == 0){
            return "";
        }
        return Stream.of(this.longBitOps)
                .map(intBitOp -> intBitOp.toSqlFilter(fieldName))
                .collect(Collectors.joining(" or ","(",")"));
    }
}

public class LongBitNotOp implements LongBitOp {
    // 拼接 <> 語句
    @Override
    public String toSqlFilter(String fieldName) {
        LongMaskOp longMaskOp = (LongMaskOp) longBitOp;
        return new StringBuilder()
                .append("(")
                .append(fieldName)
                .append(" & ")
                .append(longMaskOp.getMask())
                .append(")")
                .append("<>")
                .append(longMaskOp.getMask())
                .toString();
    }
}

整體調整完成,一起看下實際效果:

LongBitOp bitOp1 = LongMaskOp.getByBitIndex(1);
LongBitOp bitOp2 = LongMaskOp.getByBitIndex(2);
LongBitOp bitOp3 = LongMaskOp.getByBitIndex(3);

// 直接過濾 
Assertions.assertEquals("(type & 1)=1", bitOp1.toSqlFilter("type"));
Assertions.assertEquals("(type & 2)=2", bitOp2.toSqlFilter("type"));
Assertions.assertEquals("(type & 4)=4", bitOp3.toSqlFilter("type"));

// not 條件過濾
Assertions.assertEquals("(type & 1)<>1", bitOp1.not().toSqlFilter("type"));
Assertions.assertEquals("(type & 2)<>2", bitOp2.not().toSqlFilter("type"));
Assertions.assertEquals("(type & 4)<>4", bitOp3.not().toSqlFilter("type"));

// and 條件過濾
Assertions.assertEquals("((type & 1)=1 and (type & 2)=2)", bitOp1.and(bitOp2).toSqlFilter("type"));
Assertions.assertEquals("((type & 2)=2 and (type & 4)=4)", bitOp2.and(bitOp3).toSqlFilter("type"));
Assertions.assertEquals("((type & 1)=1 and (type & 4)=4)", bitOp1.and(bitOp3).toSqlFilter("type"));

// or 條件過濾
Assertions.assertEquals("((type & 1)=1 or (type & 2)=2)", bitOp1.or(bitOp2).toSqlFilter("type"));
Assertions.assertEquals("((type & 2)=2 or (type & 4)=4)", bitOp2.or(bitOp3).toSqlFilter("type"));
Assertions.assertEquals("((type & 1)=1 or (type & 4)=4)", bitOp1.or(bitOp3).toSqlFilter("type"));

5. 整體設計

打完收工,至此,我們擁有了一個功能強大的 Bit 位操作工具,不僅能夠在內存數據中進行運算,還可以生成 sql 片段,在數據庫中完成數據過濾。

最後,一起看下整體設計:

這就是封裝的藝術,不知道你能 get 多少。

  1. 項目信息

項目倉庫地址:https://gitee.com/litao851025/lego

項目文檔地址:https://gitee.com/litao851025/lego/wikis/support/bitop

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6aKNJVkNj-scoQuWfSrInA