【小家java】Java中二進位制與位運算(“^,&,>>,>>>”),使用移位演算法寫一個流水號生成器(訂單號生成器)
相關閱讀
每篇一句
高樓大廈,都是平地起的。
整個java體系,其實就是一本祕籍,那就是:java基礎!
(基礎如果打的紮實,在實際開發工作中會帶來極大的助益)
二進位制
二進位制是計算技術中廣泛採用的一種數制。二進位制資料是用0和1兩個數碼來表示的數。它的基數為2,進位規則是“逢二進一”,借位規則是“借一當二”。
0、1是基本算符。因為它只使用0、1兩個數字符號,非常簡單方便,易於用電子方式實現
如:
求 1011(2)+11(2) 的和?
結果為: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+隨機字串來拼接一個流水號。但是今天有個我認為比較優雅方式來實現。
什麼叫優雅:可以參考淘寶、京東的訂單號,看似有規律,其實沒規律
- 不想把相關資訊直接暴露出去。
- 通過流水號可以快速得到相關業務資訊,快速定位問題(這點非常重要)。
- 使用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