閱讀642 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《Java特種兵》1.3 簡單數字遊戲玩一玩

1.3 簡單數字遊戲玩一玩

數字遊戲沒錯就是玩數字遊戲

Java怎麼玩馬上見證下

玩數字有什麼用途呢我們不是虛擬數據給別人看而是通過玩數字轉換讓我們更了解計算機的數字運算也許數字運算可以有一些神奇的地方有些變態的問題也不是我們想的那麼簡單。

這裏不講基本的“四則運算”胖哥會講一些運算符然後再講講“大數字”是如何處理的。

1.3.1 變量A、B交換有幾種方式

胖哥認為有3種方法來實現變量交換其中一種最簡單的方法就是定義一個變量C作為中間量來實現代碼例子如下

int C = A;

A = B;

B = C;

有人開始不想用“定義一個變量C作為中間量”來實現而是嚐試用“數據疊加後再減回來”的方法

A = A + B;

B = A – B;

A = A – B;

這樣也可以實現A首先變成A與B的和然後減掉B自然就得到A的值再賦值給B此時B存儲了A原先的值再用A減掉B就得到原來B的值了。不過這個方法有一個問題就是A + B這個操作有可能會越界越界後就會出現問題。

既然可能越界聰明的小夥伴們又想到了另一個辦法來解決這個問題那就是“異或”運算

A = A ^ B;

B = A ^ B;

A = A ^ B;

異或運算是按照二進製位進行“異或”的對於A、B兩個數字如果某個二進製位上的A、B都是0或都是1則返回0如果一個是0一個是1則返回1這樣可以得到一個新的數字。這有什麼用呢當這個數字再與A發生“異或”的時候就能得到B這個大家可以自行反推。這樣的技巧其實是二進製位上的一個加法但不進位的做法由於隻有0、1所以1+1=2後還是為0但是這個運算是最低級的CPU位運算所以它的效率是極高的而且不會越界。

“異或”運算在計算機係統中大量存在它的一個很大的優勢就是可以按照反向的規則還原數據本身。

1.3.2 將無序數據Hash到指定的板塊

假如有一個長度為5000的數組每個數組下標就像一個Hash桶那樣存放一個鏈表此時有一個數據A需要寫入隨機嗎肯定不行因為我們還要查詢數據。那麼將它放在Hash桶裏麵可以嗎似乎可以下麵一起來探討一下。

我們希望寫入的數據能按照某種規則讀取出來那麼數據應當與數組的下標產生某種關係這樣寫入和讀取隻要按照相同的規則就可以達到目的。此時假設一個“非負數”要寫入或許可以這樣做將這個數據%5000就可以得到下標了這樣在數字不斷變化時得到的下標也會不斷變化數據自然會相對均勻分布到5000個數組中當然有特殊情況這裏不做探討。如果是負數則需要采用取絕對值Math.abs(A) % 5000來得到下標。

還有沒有其他的方法呢單純要將數據寫入到數組中可以使用A &4999來完成這個操作得到的數據將會永遠<=4999因為它自身二進製位為0的部分&操作後都會被清除為0。理論上可以實現我們的要求而且這種位操作也是最低級的係統運算效率也是極高的。

隻是這樣的方法會有一個問題什麼問題呢此時我們用數字11來說明會更好一些如果數組的長度是11那麼下標就是0-10自然輸入的數據要和10來做&操作才能得到0-10之間的數字下標10的二進製位是“1010”那麼意味著輸入的任何數字與10進行&操作後都得不到0001、0100、0101這些數字也就是對應到十進製的數字1、8、9。宏觀上講如果數組長度是11按照這種方式存入數據那麼1、8、9這幾個數組下標下麵永遠也沒數據我們預想的是想將數據離散到0-10這些位置但是有3個位置卻永遠沒數據這就是問題所在而問題的根本就是二進製位置為0的地方將永遠離散不到因此要使用這樣的方法最好選擇相應二進製位都是1的數字如1、127、255、511、1023等。

1.3.3 大量判定“是|否”的操作

如果發現一個業務係統中的許多操作都是在判定是、否很多時候大家為了節約空間不會為每一個是、否的值用一個單獨int來存儲因為那樣比較浪費空間。而一個int是4個字節所以它可以存放32bit二進製位一個二進製位就可以代表一個“是|否”。

這樣的設計是沒有問題的而且很多小夥伴們都知道這樣的設計但是胖哥發現有些可愛的同學在設計過後拿出來判定對應的位置是與否的時候是先用Integer.toBinary String(int)轉換為二進製字符串然後從這個字符串中取出對應位置的char再判定這個char是’0’還是’1’的操作。

這種操作顯然有點過了而且操作的時間複雜度很高或許我們可以學一學Java提供的一些類中是如何處理的。例如“java.lang.reflect.Modifier”它對類型的修飾符有各種各樣的判定例如字節碼中存儲public final static這樣的一長串內容隻用一個數字來表達那麼它是如何做到的呢請聽胖哥慢慢道來Java將這些符號進行了固定的規格編碼就像商品類型的名稱與編號一樣這個編碼會在生成class文件時存在JVM運行時也會使用。

◎ 是否為public使用十六進製數據0x00000001表示。

◎ 是否為static使用十六進製數據0x00000008表示。十六進製的8代表二進製的最後3位是100。

◎ 是否為final使用十六進製數據0x00000010表示。十六進製的10代表二進製最後4位為1000。

在Modifier類中大家可以找到private、protected、synchronized、volatile、transient、native、interface、abstract、strict等各種類型的表達數字。

Java程序在讀取字節碼時讀取到一個數字該數字隻需要與對應的編碼進行“&”按位求與操作根據結果是否為0即可得到答案。設計有點小技巧吧

此時有的小夥伴就會說“很多時候需要判定它是不是public final static的這麼多類型要一個個判定好麻煩。”胖哥告訴你這種方式其實還可以打組合拳。組合拳如何打呢用“或”運算具體操作就是

final static int PUBLIC_FINAL_STATIC = PUBLIC | FINAL | STATIC;

這個值將會事先保存在程序中由於這三項內容的二進製並不衝突所以做或運算就代表三者都成立的意思。當傳入參數後隻需要與這個值做“按位求與”就能得到結果了。不過現在“按位求與”的結果不是與0比較而是與PUBLIC_FINAL_STATIC比較是否等值因為需要每個二進製位都要對應上才能證明它的3個特征都成立。所有的二進製位都必須要匹配上即使一個不匹配也不行示例如下

public static boolean isPublicFinalStatic(int mod) {

return (mod & PUBLIC_FINAL_STATIC) == PUBLIC_FINAL_STATIC;

}

1.3.4 簡單的數據轉換

數據轉換有兩種其中一種是進製轉換另一種是數字與byte[]的等值轉換。我們先來看看進製轉換。

◎ 將十進製數據轉換為“二進製的字符串”使用Integer.toBinaryString()、Long.toBinaryString()來操作。大家注意了這裏說的是二進製的字符串數字本身就是以二進製形式存儲的在UI上要看到這個數字的二進製位所以才轉換成字符串來顯示。

◎ 將十進製數據轉換為“十六進製的字符串”使用Integer.toHexString(int)來操作。類似的在float、double、long中也有相應的方法存在。

◎ 將其他進製的數據轉換為十進製數據使用Integer.parseInt(String,int)、Integer. valueOf(String,int)來完成其中第二個參數傳入的就是字符串數據。例如Integer.valueOf(“10″,16)返回的值就是16Integer.valueOf(“10″,2)返回的值是2這樣就可以實現任意進製之間的轉換了。long也提供了類似的方法。

數字與byte[]之間的轉換是什麼意思呢大家都知道在Java語言中int是由4個字節byte組成的。在網絡上發送數據的時候都是通過byte流來處理的所以會發送4個byte的內容4個byte會由高到低順序排列發送接收方反向解析。在Java中可以基於DataOutputStream、DataInputStream的writeInt(int)和readInt()來得到正確的數據代碼如圖1-6所示。

XTP_TNRA{V8L1LZWSI93)H7

圖1-6 DataXXXStream對於數字的處理

如果使用了通道相關的技術ByteBuffer則由相關的API來實現。這是原始的實現方式如果其他語言提供的API希望將這些byte位交換位置例如要求低位先發送那麼你就要自己來處理了而處理方式就可以參看DataXXXStream的處理方式。

在DataXXXStream的類中不僅僅有對int類型的處理還有對boolean、byte、unsigned byte、short、unsigned short、char、int、long、float、unsigned float、double、UTF的處理而且還有一個readFull()方法這個方法要求讀滿一個緩衝區後才返回數據它會在內部區循環處理。

在ByteBuffer的實現類中也提供了各種數據類型的put、get操作內部也是通過byte轉換來完成的。

在java.io.Bits、java.nio.Bits類中也有大量的數字與byte[]的轉換操作這兩個類我們的程序無法直接使用但是可以看源碼大家可以參考它們的代碼來實現自己的特定需求。

1.3.5 數字太大long都存放不下

long采用8個字節來存放數字它連時間的毫秒值、微秒值甚至納秒值都可以存放下它存放不下的數字很少見但是它畢竟隻有8個字節如果真的遇到了某些變態的設計怎麼辦呢此時需要使用BigDecimal來操作它不是以固定長度的字節來存儲數據而是以對象的方式來管理位的。我們用一個例子來看看使用BigDecimal對一個大數字操作包含幾個步驟。

首先將大數字加上10輸出結果。

然後將結果的byte位取出來以二進製形式展現。

最後根據二進製字符串反轉為數字對象。

代碼清單1-4 BigDecimal測試

01 import java.math.BigDecimal;
02 import java.math.BigInteger;
03  
04 public class BigNumberTest {
05  
06     private static String lPad(String now ,
07                         int expectLength ,
08                         char paddingChar) {
09         if(now == null || now.length() >= expectLength) {
10             return now;
11         }
12         StringBuilder buf = new StringBuilder(expectLength);
13         for(int i = 0 , paddingLength = expectLength - now.length();
14                             i < paddingLength ; i++) {
15             buf.append(paddingChar);
16         }
17         return buf.append(now).toString();
18     }
19  
20     public static void main(String []args) {
21         //這個數字long是放不下的
22         BigDecimal bigDecimal
23 new BigDecimal("1233243243243243243243243243243243241432423432");
24         System.out.println("數字的原始值是" + bigDecimal);
25  
26         //bigDecimal = bigDecimal.add(BigDecimal.TEN);
27         //System.out.println("添加10以後" + bigDecimal);
28  
29         //二進製數字
30         byte[] bytes = bigDecimal.toBigInteger().toByteArray();
31         for(byte b : bytes) {
32             String bitString = lPad(Integer.toBinaryString(b & 0xff) ,
33                                     8 '0');
34             System.out.println(bitString);
35         }
36         //還原結果
37         BigInteger bigInteger = new BigInteger(bytes);
38         System.out.println("還原結果為" + bigInteger);
39     }
40 }

在這段代碼中有一個substring()、lPad()操作。這是因為Integer.toBinaryString(b)傳入的雖然是byte但是會轉型為int如果byte的第1個bit位是1則代表是負數那麼轉換為int的高24位將會填充1。其實我們不需要這24位所以用了substring()。如果是正數那麼輸出的字符串會將前麵的0去掉為了在顯示上使用8位二進製對齊方式所以在代碼中用了lPad()。我們再來看看輸出結果。

數字的原始值是1233243243243243243243243243243243241432423432

添加10以後1233243243243243243243243243243243241432423442

00110111

01001100

11110000

00101001

11111111

10001010

00010101

00001101

00011100

01111111

00101001

00000001

01000111

01000110

10110011

01111000

01100100

00011100

00001000

還原結果為1233243243243243243243243243243243241432423442

數字能轉換為二進製數據又能還原成數字理論上應該沒有問題大家也可以用一些比較小的數字做測試來印證這個結論例如2、15、16、1024等。不過在印證的過程中有的同學發現高位出現整個字節的8位全是0的情況例如255輸出的結果是兩個字節的二進製字符串0000000011111111。大家很疑惑既然高8位全是0為何還要單獨拿一個byte來存放呢有一點要注意了因為255是正數而一個字節中的最高位變成了1如果直接用11111111來表達就表示它是一個負數具體值是-1所以需要多一個字節來表達自己是正數。

long的存儲能力到底有多大

許多同學設計一些PK列的時候認為數字存儲不下用字符串來存放數字當然不排除某種編碼規則但是用數字往往計算快速得多而且空間也少很多。為了了解long的存儲能力有多大我們先看看int的存儲能力有多大。Integer.MAX_VALUE的值為“2147483647”也就是2G-1的大小。如何得來自然是4字節對應32位然後去掉負數和0得來的。這個值換算成十進製數據就是21億理論上很多表達不到那麼大或者說絕大部分表不會有這麼多ID但是若某些跳躍和曆史數據超過這個數字怎麼辦那麼就用long吧它的寬度是int的2倍即4G*4G/2–1為什麼自己想想。十六進製數據0x7fffffffffffffffL換算為十進製數據是“9223372036854775807”這個數字看不出到底有多大我們可以和時間進行比較。

Long.MAX_VALUE / (365l * 24 * 60 * 60 * 1000) = 292471208

Long.MAX_VALUE / (365l * 24 * 60 * 60 * 1000 * 1000) = 292471

Long.MAX_VALUE / (365l * 24 * 60 * 60 * 1000 * 1000000) = 292

如果存放毫秒值則可以存放2.92億年微妙值可以存放29.2萬年納秒值可以存放292年的數據。換句話說一個表的ID即使加上跳躍每秒有10E9個ID自動增長也可以增加292年的數據不重複胖哥認為這種並發在現在的計算機時代還不存在。小夥伴們通過這種量化後自然應該理解某些設計應該如何做了吧。

數字遊戲玩到這裏就結束了在本書的後文中胖哥還會提到一些和數字相關的小玩意例如i++與++i的問題或許胖哥的說法與在教材中看到的解釋不一樣哦。

最後更新:2017-05-23 14:32:52

  上一篇:go  阿裏雲雲合計劃加速落地 聯合思科、中標軟件推商業軟件免費試用計劃
  下一篇:go  深度解析Java8 – AbstractQueuedSynchronizer的實現分析(下)