1. 程式人生 > 其它 >java bigdecimal負數轉正數_Java 大神的十個私藏避坑絕技

java bigdecimal負數轉正數_Java 大神的十個私藏避坑絕技

技術標籤:java bigdecimal負數轉正數

28afcfe1e66e77095381082b9811a70c.png

注:本文來自粉絲[菜鳥逆襲]投稿

1.奇數性

看下面程式碼時候是否能判斷引數 i 是奇數?

publicstaticbooleanisOdd(inti){
returni%2==1;
}

答案是: No!
看似正確的判斷奇數, 但是如果 i 是負數, 那麼它返回值都是false
造成這種現象的是 => 從思想上固化, 認為奇數只在正數範圍, 故判斷負數將報錯, 在C++中也是, 負數取餘還是負.
在Java中取餘操作定義產生的後果都滿足下面的恆等式:

int數值a,與非零int數值b都滿足下面的等式:
(a/b)*b+(a%b)==a

從上面就可以看出, 當取餘操作返回一個非零的結果時, 左右運算元具有相同的正負號, 所以當取餘在處理負數的時候, 以及會考慮負號.

而上面的這個問題, 解決方法就是避免判斷符號:

publicstaticbooleanisOdd(inti){
returni%2!=0;
}

讓結果與0比較, 很容易避免正負號判斷.思考:
1.在使用取餘操作的時候要考慮符號對結果的影響
2.在運算中, 嘗試使用0解決符號問題, 在一定程度上避免符號對結果的影響

2.浮點數產生的誤差

看下面程式碼會打印出什麼樣的結果?

publicclassChange{
publicstaticvoidmain(Stringargs[]){
System.out.println(2.00-1.10);
}
}

從主觀上看, 列印的結果必然是0.90, 然後這卻是一個主觀錯誤.
對於1.10這個數, 計算機只會使用近似的二進位制浮點數表示, 產生精度影響.
從上面的例子中來看, 1,10在計算機中表示為1.099999, 這個1.10並沒有在計算機中得到精確的表示.
針對這個精度問題, 我們可能會選擇: System.out.printf("%.2f%n", 2.00 - 1.10);解決, 儘管打印出來的是正確答案, 但是依舊會暴露出一個問題: 如果精度控制在2.00 - 1.0010; 那麼精度誤差依舊會出現.
這裡也說明了: 使用printf, 計算機底層依舊是使用二進位制的方式來計算, 只不過這種計算提供了更好的近似值而已.

那麼應該怎麼解決這個問題呢?
首先想到是使用int模擬小數每一位, 然後計算, 最後將結果又轉化為小數;
以此想到的就是使用BigDecimal類, 它主要用於精確小數運算.

importjava.math.BigDecimal;
publicclassChange1{
publicstaticvoidmain(Stringargs[]){
System.out.println(newBigDecimal("2.00").subtract(newBigDecimal("1.10")));
}
}

通過上面的程式碼就能得到一個精確的值.注: 使用BigDecimal的時候, 不要使用BigDecimal(double d)的構造方法, 在double與double之間傳值的時候依舊會引起精度損失. 這是一個嚴重的問題.
BigDecimal底層採用的就是int[], 使用String的時候, 會將String不斷取每一位存入int[], 使用double的時候, 同理將數字的每一位存入int[], 但是double本身存在誤差, 導致存入的資料會出現誤差,例: 0.1存入double就表示為0.1000000099999999, 因此不使用double型別的建構函式思考:
當然對於精確要求不高的地方, 完全可以使用float/double, 但是對於要求精度的計算, 比如貨幣 一定要使用int, long, BigDecimal.

3.長整數造成資料溢位

看下面的程式碼會列印什麼?

publicclassLongDivision{
publicstaticvoidmain(Stringargs[]){
finallongMICROS_PER_DAY=24*60*60*1000*1000;
finallongMILLIS_PER_DAY=24*60*60*1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}

整個過程, 除數與被除數都是long型, 很容易儲存這兩個數, 結果一定是1000, 但是結果讓你失望了, 結果是5.
這又是怎麼回事呢?
首先這個表示式: 24606010001000總是在int型別的基礎上進行計算. 即表示式是按照int的規則計算.
很容易看出這個表示式計算的範圍早已超出int的取值範圍, 縱然使用long去儲存計算結果, 但是在計算的過程中就已經出現計算資料溢位, 這是一個隱藏錯誤.
Java目標確定型別的特性 => 如上例子, 不同通過 long 去確定24606010001000按照long進行儲存.必須指定資料型別, 才能按照指定的規則進行運算.
就用前面這個例子來看, 當指定24為24L就能防止資料計算溢位, 在進行乘法運算的時候就已經是在long的基礎上進行計算.

publicclassLongDivision{
publicstaticvoidmain(Stringargs[]){
finallongMICROS_PER_DAY=24L*60*60*1000*1000;
finallongMILLIS_PER_DAY=24L*60*60*1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}

思考:
這個問題給了我一個深刻的教訓, 當運算元很大的時候, 要預防運算元溢位, 當無法確定計算數會不會溢位, 所要做的就是用儲存範圍最大的型別: long 來進行計算。

4.long的 "L" 與 "l" 所引發的錯誤

從上面 "長整數運算造成資料溢位" 引發又一個問題, 看下面例子:

publicclassElementary{
publicstaticvoidmain(String[]args){
System.out.println(12345+5432l);
}
}

乍一看, 這很簡單, 計算結果時是 6666, 但是列印的結果是 17777, 我開始頭暈了, 這很不合理.
思考過後, 發現了一個問題:
我把 "l" 看作是 "1", "l" 只是用於標識5432是一個long型別, 這個視覺上的錯誤將會引發更嚴重的問題.思考:
小寫字母 l 與 1 很容易造成混淆, 為了避免這種錯誤, 在表示long型別資料的, 要做的就是將 "l" 換做 "L", 掐斷產生混亂的源頭.

5.多重型別轉換引發的數值變化

看這樣的一個例子:

publicclassMulticast{
publicstaticvoidmain(String[]args){
System.out.println((int)(char)(byte)-1);
}
}

看似結果是 -1, 但是執行之後, 結果變為 65535分析一下:

byte下的-1=>變為:
1111,1111,1111,1111,1111,1111,1111,1111
32位(4個位元組)首位1表示負號.

byte到char=>變為:
0000,0000,1111,1111
16位(2個位元組),首位0,就此負號變正號.

char到int=>變為:
0000,0000,0000,0000,0000,0000,1111,1111
32位(4個位元組)

由此可見, 在byte到char的變化過程中出現符號轉換的問題. char首位總是0使得負號變正號.型別轉換的過程存在這樣的簡單規則: 如果最初的數值型別是有符號的,那麼就執行符號擴充套件;如果它是 char,那麼不管它將要被轉換成什麼型別,都執行零擴充套件。因此這也就解釋了為什麼byte到char的過程存在負號變正號.
為了在轉換的過程中保留符號, 就使用位掩碼進行限制, 例如:

charc=(char)(b&0xff);

這樣就能保證符號具有保留思考:
在對有符號與無符號之間的轉換, 一定要注意上面的轉換規則, 如果不能確定轉換符號是否正確, 那麼就避免出現有符號到無符號之間的轉換.

6.避免所謂聰明的程式設計技巧

對於交換兩個變數的內容, 在C/C++中存在一種這樣的程式設計技巧:

intx=1111;
inty=2;
x^=y^=x^=y;
cout<""<

這樣一個簡單的連續異或就能解決變數的交換問題, 這種寫法在很久以前是為了減少臨時變數的使用, 所以這種做法在現在也得到了保留.
首先看這樣一個問題, 表示式x^=y, 在C/C++的編譯器中是先計算出y的值, 然後再獲取x的值, 最後再計算表示式. 但在Java中的做法是先獲得x的值, 再獲得y的值, 最後再計算.Java的語言規範描述: 操作符的運算元是從左往右求值.
這使得在計算 x^ =y^ =x^ =y表示式中的第二個x的時候是在計算x^ =y之前的值( x的值依舊是1111 ), 並不是x^=y後的值, 這就導致了計算上的錯誤.
所以在Java中準確的寫法是:

y=(x^=(y^=x))^y

思考:
上面的這種寫法極其容易引起錯誤, 程式的可讀性受到很大的影響, 所以在寫程式碼的時候要思考一個問題, 除非編譯器能確定運算元的運算順序, 不然不要讓編譯器去確定運算元的計算順序, 就比如這樣的表示式: x=a[i]++-a[j]++. 很容易導致錯誤.

7.避免使用混合運算

看如下程式碼:

publicclassDosEquis{
publicstaticvoidmain(String[]args){
charx='X';
inti=0;
System.out.println(true?x:0);
System.out.println(false?i:x);
}
}

看似將列印: XX, 但是結果卻是X88.
這是一個出乎意料的結果.
思考之後, 將可能得出這樣的結論: 出現這樣問題的原因是運算元的型別自動提升, char=>int.
但是又有一個問題就是為什麼第一個運算不是88. 找其根本原因還是在於條件表示式的運算規則:

A?B:C
B,C為相同型別,那麼表示式的計算結果就是B,C的型別
B,C不是相同型別的時候,那麼計算結果就按照B的型別(此時B必須是式子中最高型別).
此時C的型別就自動上升為式子中最高的型別,例:false?x:i,輸出是0,而不是0對應的字元.

上面的規則決定了, 將呼叫哪一個print的過載函式.
這種條件表示式返回值, 很容易受B, C型別影響. 當根據返回值作條件判斷的時候, 這種性質也將導致一個嚴重的問題.思考:
上面的問題說明了, 在條件表示式中, 最後再後兩個運算元使用相同型別的運算元, 以此避免返回值型別不確定的問題, 並且在其他的表示式計算中, 一定要理清楚數值之間的型別轉換.

8.發現隱藏的型別轉換

在這樣的表示式: x += i; 按照平常的理解, 它一定是x = x + i; 可是這樣的運算表示式是建立在x與i具有相同的型別的基礎上的, 如果當x, i型別不相同的時候, 將會引發一個問題就是精度損失.
就比如:

shortx=0;
inti=99999;
x+=i;

現在的x不是99999, 而是-31073.
當 x += i 的時候, 出現的問題就是i自動轉型為short, 此時x的值就不再是99999. 而當你將表示式寫為: x = x + i 的時候, 這是一種顯式的轉型, 自然需要強轉操作. 從而避免了隱藏的型別轉換.思考:
複合運算會隱藏出現轉型操作, 這種轉型操作很有可能出現精度丟失.
所以在進行復合運算的時候, 避免兩邊的運算元是不同的型別, 防止編譯器出現危險的窄化型別, 或者不使用複合運算, 人為進行型別轉換.

9.字串的"+"運算子

看如下程式碼:

publicclassLastLaugh{
publicstaticvoidmain(String[]args){
System.out.print("H"+"a");
System.out.print('H'+'a');
}
}

由於長期受 "+" 運算子的影響, 上面的程式碼, 很容易把 'H'+'a' 也看作是字串的連線, 這是一種慣性的思考方式.
在 'H'+'a' 表示式的運算中, 是將 'H', 'a', 上升為int, 進行數值運算.
如果想讓兩個字元連線在一起, 可以採用:

1.使用StringBuffer/StringBuild做append運算.
StringBuilds="".append('H');
2.使用Strings=""+'H'+'a';使用字串連線.
Strings1=""+'H'+'a';
Strings2='a'+'H'+"";
System.out.println(s1);
System.out.println(s2);
注:避免s2的寫法,這樣寫'a'+'H'依舊做int的數值運算

思考:
在使用 "+" 運算子一定要注意運算元的型別, 以防止慣性思維導致的運算錯誤. 在某些場合這種錯誤有可能是致命性的.

看完字元的 "+" 運算子, 現在再來字元陣列的 "+"運算子 :

publicclassA{
publicstaticvoidmain(String[]args){
Stringletters="ABC";
char[]numbers={'1','2','3'};
System.out.println(letters+"easyas"+numbers);
}
}

上面的程式碼, 最終的列印結果不是 ABC easy as 123, 而是ABC easy as [[email protected]
如果想到的列印結果是ABC easy as 123, 那麼犯的錯誤還是上面相同的錯誤.
在列印結果的時候, 首先會進行字串連線, 當 "easy as" 這個字串連線 char[] 的時候, 那麼呼叫的是char[] 的toString(), 而系統並沒有重寫toString(), 所以最後呼叫的就是Object的toString();


為了修正這樣的錯誤, 給出如下解決方式:

1.使用String.valueOf(number);轉字串後再進行連線操作.
2.使用System.out.println(number);呼叫過載的println(char[]c);

而在C/C++中, char numbers[4] = {'1', '2', '3', '\0' }; 代表的就是一個字串.思考:
牢記, 陣列型別的toString都沒有重寫, 如果想獲得陣列中的值, 避免呼叫陣列型別的toString, 或者讓系統隱藏呼叫, 而是直接遍歷陣列獲得其中的值.

10."=="運算子進行比較

  • 問題1:
    這裡先說明第一個問題, 就是Java中的 "==" 運算子: 在比較基本型別的時候, 是比較基本型別值的關係; 在比較陣列, 或者物件的時候是比較物件之間的引用值關係. 但是這裡要注意的是:
    在比較Integer, Long(本人親測)這兩種的時候, 比較-128~127的時候是從快取池中拿取資料.

Integer中的equals方法:

publicbooleanequals(Objectobj){
if(objinstanceofInteger){
returnvalue==((Integer)obj).intValue();
}
returnfalse;
}

這個過程中實現的是將Integer拆包,-128~127不需要拆包,可直接使用==比較.

Integer的快取池-128~127: 自動裝箱過程中使用valueOf建立物件,因此直接會使用快取池中物件.思考:
這裡我想表達的意思就是, 如果要進行物件內容之間的比較, 務必重寫equals, 然後使用equals. 還有避免在基本型別與包裝型別混合狀態的基礎上使用 "==", 就比如 Integer. 這個很容易導致錯誤

  • 問題2:
    當看到這樣的程式碼的時候:

publicclassAnimalFarm{
publicstaticvoidmain(String[]args){
finalStringpig="length:10";
finalStringdog="length:"+pig.length();
System.out.println("Animalsareequal:"+pig==dog);
}
}

我想去比較pig與dog引用值關係, pig 與 dog 的引用值肯定是相同的, 但是最後的輸出結果卻是false.
在這裡忽略了一個問題, 那就是前面的 "+" 的運算級比 "==" 的運算級高, 看似是比較pig與dog的引用值, 最後卻是比較"Animals are equal: length: 10"與dog的引用值關係.
現在給出下面的修正方案:

1.System.out.println("Animalsareequal:"+(pig==dog));
2.System.out.println("Animalsareequal:"+pig.equals(dog));

思考:
從這裡也看出, 比較物件內容的時候, 務必使用已經過載後equals, 除非刻意比較兩個物件的引用值, 否則千萬別使用"==".

往期推薦文章

[校招系列]:

網際網路應屆生簡歷編寫指南 No.101hot

應屆生的認知誤區 No.107

憑什麼你拿不到好offer No.108

校招,另一場千軍萬馬的廝殺No.83

[軟體質量系列]:

月薪50K的測試,背鍋的姿勢比你優雅(1) No.163

月薪50K的測試,背鍋的姿勢比你優雅(2) No.164