1. 程式人生 > >【小家java】Java中二進位制與位運算(“^,&,>>,>>>”),使用移位演算法寫一個流水號生成器(訂單號生成器)

【小家java】Java中二進位制與位運算(“^,&,>>,>>>”),使用移位演算法寫一個流水號生成器(訂單號生成器)

相關閱讀

每篇一句

高樓大廈,都是平地起的。

整個java體系,其實就是一本祕籍,那就是:java基礎!

(基礎如果打的紮實,在實際開發工作中會帶來極大的助益)

二進位制

二進位制是計算技術中廣泛採用的一種數制。二進位制資料是用0和1兩個數碼來表示的數。它的基數為2,進位規則是“逢二進一”,借位規則是“借一當二”。

0、1是基本算符。因為它只使用0、1兩個數字符號,非常簡單方便,易於用電子方式實現

如:

10112+112) 的和?
結果為:1110(二進位制數)

二進位制、八進位制、十進位制與十六進位制,它們之間區別在於數運算時是逢幾進一位。
在這裡插入圖片描述

進位制轉換

由於市面上已經有很多解釋了,因此本文不再重複造輪子。推薦百度經驗的一篇文章:

二進位制、八進位制、十進位制、十六進位制之間的轉換

二進位制與編碼
  • 一般對英文字元而言,一個位元組表示一個字元,但是對漢字而言,由於低位的編碼已經被使用(早期計算機並不支援中文,因此為了擴充套件支援,唯一的辦法就是採用更多的位元組數)只好向高位擴充套件。
  • 一般字符集編碼的範圍 utf-8>gbk>iso-8859-1(latin1)>ascll。ascll編碼是美國標準資訊交換碼的英文縮寫,包含了常用的字元,如阿拉伯數字,英文字母和一些列印符號,請注意字元和數字的區別,比如’0’字元對應的十進位制數字是48。
  • unicode編碼包含很多種格式,utf-8是其中最常用的一種,utf-8名稱的來自於該編碼使用8位一個位元組表示一個字元
    。對於一個漢字而言,它需要3個位元組表示一個漢字,但大中華地區人民表示不服,搞一套gbk編碼格式,用兩個位元組表示一個漢字。

因此:計算效率最高

Java中二進位制

Java7之前是不支援前置直接表示二進位制數的,但現在可以了。
二進位制:前置0b/0B
八進位制:前置0
十進位制:預設的,無需前置
十六禁止:前置0x/0X

    public static void main(String[] args)  {
        //備註 System.out.println()都會自動轉為10禁止輸出的

        //二進位制
        int i = 0B101;
        System.out.
println(i); //5 //八進位制 i = 0101; System.out.println(i); //65 //十進位制 i = 101; System.out.println(i); //101 //十六進位制 i = 0x101; System.out.println(i); //257 }
Java中的進位制轉換
    public static void main(String[] args) {

        int i = 192;
        System.out.println("---------------------------------");
        System.out.println("十進位制轉二進位制:" + Integer.toBinaryString(i)); //11000000
        System.out.println("十進位制轉八進位制:" + Integer.toOctalString(i)); //300
        System.out.println("十進位制轉十六進位制:" + Integer.toHexString(i)); //c0
        System.out.println("---------------------------------");
        // 統一利用的為Integer的valueOf()方法,parseInt方法也是ok的
        System.out.println("二進位制轉十進位制:" + Integer.valueOf("11000000", 2).toString()); //192
        System.out.println("八進位制轉十進位制:" + Integer.valueOf("300", 8).toString()); //192
        System.out.println("十六進位制轉十進位制:" + Integer.valueOf("c0", 16).toString()); //192
        System.out.println("---------------------------------");
    }

怎麼證明Long型別是64位呢?

其實最簡單的 我們看看Long型別的最大值,用2進製表示轉換成字串看看長度就行了

    public static void main(String[] args) {

        long l = 100L;
        //如果不是最大值 前面都是0  輸出的時候就不會有那麼長了
        System.out.println(Long.toBinaryString(l)); //1100100
        System.out.println(Long.toBinaryString(l).length()); //7

        l = Long.MAX_VALUE;
        //正數長度為63為
        System.out.println(Long.toBinaryString(l)); //111111111111111111111111111111111111111111111111111111111111111
        System.out.println(Long.toBinaryString(l).length()); //63

        l = Long.MIN_VALUE;
        //負數長度為64位
        System.out.println(Long.toBinaryString(l)); //1000000000000000000000000000000000000000000000000000000000000000
        System.out.println(Long.toBinaryString(l).length()); //64

    }

相對應的,可以簡單看看int的範圍:

    public static void main(String[] args) {

        int i = Integer.MAX_VALUE;
        //正數長度為63為
        System.out.println(Integer.toBinaryString(i).length()); //31

        i = Integer.MIN_VALUE;
        //負數長度為64位
        System.out.println(Integer.toBinaryString(i).length()); //32
    }

2進位制第一位都是符號位,0表示正數1表示負數

Java中位運算子的使用

在Java中存在著這樣一類操作符,是針對二進位制進行操作的。它們各自是&、|、^、~、>>、<<、>>>幾個位操作符。不管是初始值是依照何種進位制,都會換算成二進位制進行位操作。

&:按位與

操作的規則是:僅當兩個運算元都為1時。輸出結果才為1。否則為0

    public static void main(String[] args) {
        System.out.println(2 & 3); // 2
        //10 & 11 = 10 因此結果為2
    }
|:按位或

操作的規則是:僅當兩個運算元都為0時,輸出的結果才為0。

    public static void main(String[] args) {
        System.out.println(2 | 3); // 3
        //10 | 11 = 11 因此結果為3
    }
^:按位異或

異或運算操作的規則是:僅當兩個運算元不同的時候。對應的輸出結果才為1,否則為0,示比例如以下:

    public static void main(String[] args) {
        System.out.println(2 ^ 3); // 1
        //10 ^ 11 = 01 因此結果為1
    }
~:按位取反

操作規則為:全部的0置為1,1置為0,示比例如以下:

    public static void main(String[] args) {
        System.out.println(~12); // -13
        //~ 1100 = 10000000 00000000 00000000 00001101 因此結果為-13
    }
<<:左移

左移就是把一個數的全部位數都向左移動若干位,示比例如以下:

    public static void main(String[] args) {
        System.out.println(2 << 3); // 16
        //10 << 3 向左移動三位為10000  轉化為十進位制為1 * 2的四次方 = 16
    }

左移用得非常多,也非常好理解。x左移多少位,就相當於乘以2的多少次方就行了

>>:右移

右移就是把一個數的全部位數都向右移動若干位

    public static void main(String[] args) {
        System.out.println(2 >> 3); // 0
        //10 >> 3 向右移動三位 位數根本不夠 所以直接就為0了
        System.out.println(100 >> 3); //12
        //1100100 >> 3 向右移動三位為1100 轉換為十進位制為8+4 = 12
        System.out.println(Integer.toBinaryString(100));
    }

右移用得也很多,操作其實就是吧右邊的N位直接砍掉即可

>>>:無符號右移(注意:沒有無符號左移)

無符號右移,忽略符號位,空位都以0補齊。

10進位制轉二進位制的時候,因為二進位制數一般分8位、 16位、32位以及64位 表示一個十進位制數,所以在轉換過程中,最高位會補零。

在計算機中負數採用二進位制的補碼表示,10進位制轉為二進位制得到的是原始碼,將原始碼按位取反得到的是反碼,反碼加1得到補碼

    public static void main(String[] args) {
        byte byteValue = -1;
        System.out.println(Integer.toBinaryString(byteValue)); //11111111111111111111111111111111
        //byte型別轉二進位制輸出  需要& 0xff   否則是會向上轉型為int型別再處理的
        //因為Byte類沒有toBinaryString方法
        System.out.println(Integer.toBinaryString(byteValue & 0xff)); //11111111

        //此處也是一樣 如果是byte型別 需要 & 0xff再右移
        System.out.println(byteValue & 0xff >>> 4);
    }

二進位制的最高位是符號位,0表示正,1表示負。(0一般省略不寫)

    public static void main(String[] args) {
        System.out.println(16 >> 2); //4
        System.out.println(16 >>> 2); //4
    }

可見正數做>>>運算的時候和>>是一樣的。區別在於負數運算

至於本文中byte為什麼要與上0xff,請參考:byte為什麼要與上0xff?

>>>>>唯一的不同是它無論原來的最左邊是什麼數,統統都用0填充。

當我們掌握了Java中的位運算了之後,我們接下來利用位運算的可逆性,來達到隱藏資料的一些效果,並且效率也是非常的高

在JDK的原碼中。有很多初始值都是通過位運算計算的,位運算有很多特性,能夠線上性增長的資料中起到作用。且對於一些運算,位運算是最直接、最簡便的方法。

移位運算還有個很大的作用,就是用在資料庫上

其實玩法比較像Linux裡的許可權控制:許可權分為 r 讀, w 寫, x 執行,其中 它們的權值分別為4,2,1, 所以 如果使用者要想擁有這三個許可權 就必須 chomd 7 , 即 7=4+2+1 表明 這個使用者具有rwx許可權,如果只想這個使用者具有r,x許可權 那麼就 chomd 5即可

尷尬現象:通常 我們的資料表中 可能會包含各種狀態屬性, 例如 blog表中 , 我們需要有欄位表示其是否公開,是否有設定密碼,是否被管理員封鎖,是否被置頂等等。 也會遇到在後期運維中,策劃要求增加新的功能而造成你需要增加新的欄位。

這樣會造成後期的維護困難,資料庫增大,索引增大的情況。 這時使用**位運算**就可以巧妙的解決。

  • 公開blog 給status進行或運算
    UPDATE blog SET status = status | 1;
  • 加密blog 給status進行或運算
    UPDATE blog SET status = status | 2;
  • 封鎖blog
    UPDATE blog SET status = status | 4;
  • 解鎖blog
    UPDATE blog SET status = status ^ 4;
  • 查詢所有被置頂的blog
    SELECT * FROM blog WHERE status & 8;

注意:

    public static void main(String[] args) {
        System.out.println(Long.toBinaryString(10 << 61)); //1000000000000000000000000000000
        System.out.println(Long.toBinaryString(10L << 61)); //100000000000000000000000000000000000000000000000000000000000000
    }

由上面例子警示自己,當我們在移位的時候,一定要注意資料型別。一般我建議顯示的表示出來,否則容易出錯。我曾經有雪的教訓

使用位運算,不借助第三方變數方式交換兩個數的值

我們都知道java中兩個數a,b互換,需要藉助第三方變數作為臨時變數來儲存資料,再進行互換

在這裡我提供兩個不需要臨時變數的方法

    public static void main(String[] args) {
        int a = 3, b = 5;
        System.out.println(a + "-------" + b);
        a = a + b;
        b = a - b;
        a = a - b;
        System.out.println(a + "-------" + b);
    }

這樣能滿足絕大部分要求,但是:a+b,可能會超出int型的最大範圍,造成進度丟失,有風險
因此採用下面位運算方法更保險:

    public static void main(String[] args) {
        int a = Integer.MAX_VALUE, b = Integer.MAX_VALUE - 10;
        System.out.println(a + "-------" + b);
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
        System.out.println(a + "-------" + b);
    }

流水號生成器(訂單號生成器)

生成訂單流水號,當然這其實這並不是一個很難的功能,最直接的方式就是日期+主機Id+隨機字串來拼接一個流水號。但是今天有個我認為比較優雅方式來實現。
什麼叫優雅:可以參考淘寶、京東的訂單號,看似有規律,其實沒規律

  1. 不想把相關資訊直接暴露出去。
  2. 通過流水號可以快速得到相關業務資訊,快速定位問題(這點非常重要)
  3. 使用AtomicInteger可提高併發量,降低了衝突
原理介紹

此流水號構成:日期+Long型別的值 組成的一個一長串數字,形如:2018112019492195304210432,顯然前面是日期資料,後面的一長串就蘊含了不少的含義:、當前秒數、商家ID(也可以是你其餘的業務資料)、機器ID、一串隨機碼等等

各部分介紹:
1:第一部分為當前時間的毫秒值。最大999,所以佔10位
2:第二部分為:serviceType表示業務型別。比如訂單號、操作流水號、消費流水號等等。最大值定為30,足夠用了吧。佔5位
3:第三部分為:shortParam,表示使用者自定義的短引數。可以放置比如訂單型別、操作型別等等類別引數。最大值定為30,肯定也是足夠用了的。佔5位
4:第四部分為:longParam,同上。使用者一般可防止id引數,如使用者id、商家id等等,最大支援9.9999億。絕大多數足夠用了,佔30位
5:第五部分:剩餘的位數交給隨機數,隨機生成一個數,佔滿剩餘位數。一般至少有15位剩餘,所以能支援2的15次方的併發,也是足夠用了的
6:最後,在上面的long值前面加上日期時間(年月日時分秒)

上原始碼

Tips:此原始碼為本人獨立編寫,自測多種情況,若各位使用中有更好的建議,歡迎留言

/**
 * 通過移位演算法 生成流水號
 * <p>
 * --> 通用版本(其實各位可以針對具體場景 給出定製化版本  沒關係的)
 * (最直接的方式就是日期+主機Id+隨機字串來拼接一個流水號)
 *
 * @author fangshixiang
 * @description //
 * @date 2018/11/20 14:58
 */
public class SerialNumberUtil {

    //採用long值儲存 一共63位
    private static final int BIT_COUNT = 63;
    //各個部分佔的最大位數(為了減輕負擔,時分秒都放到前面去  不要佔用long的位數了  但是毫秒我隱藏起來,方便查問題)
    //毫秒值最大為999(1111100111)佔10位
    private static final int SHIFTS_FOR_MILLS = 10;
    //下面是各部分的業務位數(各位根據自己不同的業務需求  自己定製)
    //serviceType佔位
    private static final int SHIFTS_FOR_SERVICETYPE = 5;
    //shortParam佔位
    private static final int SHIFTS_FOR_SHORTPARAM = 5;
    private static final int SHIFTS_FOR_LONGPARAM = 30;

    ///////////////////////////////////
    //最後的隨機數 佔滿剩餘位數
    private static final int SHIFTS_FOR_RANDOMNUM = BIT_COUNT - SHIFTS_FOR_MILLS
            - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM - SHIFTS_FOR_LONGPARAM;


    //掩碼 用於輔助萃取出資料  此技巧特別巧妙
    private static final long MASK_FOR_MILLS = (1 << SHIFTS_FOR_MILLS) - 1;
    private static final long MASK_FOR_SERVICETYPE = (1 << SHIFTS_FOR_SERVICETYPE) - 1;
    private static final long MASK_FOR_SHORTPARAM = (1 << SHIFTS_FOR_SHORTPARAM) - 1;
    private static final long MASK_FOR_LONGPARAM = (1 << SHIFTS_FOR_LONGPARAM) - 1;

    //時間模版
    private static final String DATE_PATTERN = "yyyyMMddHHmmss";

    /**
     * 生成流水號  若需要隱藏跟多的引數進來,可以加傳參。如訂單型別(訂單id就沒啥必要了)等等
     *
     * @param serviceType 業務型別,比如訂單號、消費流水號、操作流水號等等  請保持一個公司內不要重複
     *                    最大值:30(11110) 佔5位
     * @param shortParam  短引數 不具體定義什麼  一般用於表示型別。如這表示訂單流水號,這裡可以表示訂單型別
     *                    最大值:30(11110) 佔5位
     * @param longParam   長引數,一般用於記錄id引數什麼的,比如是訂單的話,這裡可以表示商戶ID(商戶一般不會非常多吧)
     *                    最大值:999999999(101111101011110000011111111) 佔30位  表示9.999億的資料  相信作為id的話,一般都超不過這個數值吧
     * @return 流水號 年月日時分秒+long型別的數字 = string串
     */
    public static String genSerialNum(long serviceType, long shortParam, long longParam) {
        if (serviceType > 30) {
            throw new RuntimeException("the max value of 'serviceType' is 30");
        }
        if (shortParam > 30) {
            throw new RuntimeException("the max value of 'shortParam' is 30");
        }
        if (longParam > 99999999) {
            throw new RuntimeException("the max value of 'longParam' is 99999999");
        }

        //放置毫秒值
        long mills = LocalTime.now().getNano() / 1000000; //備註 此處一定要是long型別 否則會按照int的32位去移位
        long millsShift = mills << (BIT_COUNT - SHIFTS_FOR_MILLS);

        //放置serviceType
        long serviceTypeShift = serviceType << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE);

        //放置shortParam
        long shortParamShift = shortParam << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM);

        //放置longParam
        long longParamShift = longParam << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM - SHIFTS_FOR_LONGPARAM);

        //生成一個指定位數(二進位制位數)的隨機數  最後一個 不需要左移了 因為長度就是自己
        long randomShift = getBinaryRandom(SHIFTS_FOR_RANDOMNUM);

        //拼接各個部分
        long finalNum = millsShift | serviceTypeShift | shortParamShift | longParamShift | randomShift;

        //最後前面拼接上年月日時分秒 返回出去
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN)) + finalNum;
    }

    /**
     * 拿到指定位數的 首位數字不為0的位數,最終以十進位制數返回出來
     *
     * @param count 需要的總位數 總位數不允許超過63
     * @return binary random
     */
    private static long getBinaryRandom(int count) {
        StringBuffer sb = new StringBuffer();
        String str = "01";

        //採用ThreadLocalRandom 生成隨機數 避免多執行緒問題
        ThreadLocalRandom r = ThreadLocalRandom.current();
        for (int i = 0; i < count; i++) {
            int num = r.nextInt(str.length());
            char c = str.charAt(num);
            while (c == '0') { //確保第一個是不為0數字 否則一直迴圈去獲取
                if (i != 0) {
                    break;
                } else {
                    num = r.nextInt(str.length());
                    c = str.charAt(num);
                }
            }
            sb.append(c);
        }
        return Long.valueOf(sb.toString(), 2);
    }

    //===============================提供便捷獲取各個部分的工具方法===================================

    /**
     * 從序列號拿到日期 並且格式化為LocalDateTime格式
     *
     * @param serialNumber 流水號
     * @return 日期時間
     */
    public static LocalDateTime getDate(String serialNumber) {
        String dateStr = serialNumber.substring(0, DATE_PATTERN.length());
        return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern(DATE_PATTERN));
    }

    /**
     * 拿到毫秒數:是多少毫秒
     *
     * @param serialNumber 流水號
     * @return 毫秒數
     */
    public static long getMills(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS) & MASK_FOR_MILLS;
    }

    /**
     * 拿到 serviceType
     *
     * @param serialNumber 流水號
     * @return serviceType
     */
    public static long getServiceType(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE) & MASK_FOR_SERVICETYPE;
    }

    /**
     * 拿到 shortParam
     *
     * @param serialNumber 流水號
     * @return shortParam
     */
    public static long getShortParam(String serialNumber) {
        return getLon