学习Java中遇到的问题积累_1
1.奇數(shù)性
看下面代碼時候是否能判斷參數(shù) i 是奇數(shù)?
public static boolean isOdd(int i){ return i % 2 == 1; }答案是: NO
看似正確的判斷奇數(shù), 但是如果 i 是負(fù)數(shù), 那么它返回值都是false
造成這種現(xiàn)象的是 => 從思想上固化, 認(rèn)為奇數(shù)只在正數(shù)范圍, 故判斷負(fù)數(shù)將報錯, 在C++中也是, 負(fù)數(shù)取余還是負(fù).
在Java中取余操作定義產(chǎn)生的后果都滿足下面的恒等式:
從上面就可以看出, 當(dāng)取余操作返回一個非零的結(jié)果時, 左右操作數(shù)具有相同的正負(fù)號, 所以當(dāng)取余在處理負(fù)數(shù)的時候, 以及會考慮負(fù)號.
而上面的這個問題, 解決方法就是避免判斷符號:
讓結(jié)果與0比較, 很容易避免正負(fù)號判斷.
思考:
1.在使用取余操作的時候要考慮符號對結(jié)果的影響
2.在運算中, 嘗試使用0解決符號問題, 在一定程度上避免符號對結(jié)果的影響
2.浮點數(shù)產(chǎn)生的誤差
看下面代碼會打印出什么樣的結(jié)果?
public class Change{ public static void main(String args[]){ System.out.println(2.00 - 1.10); } }從主觀上看, 打印的結(jié)果必然是0.90, 然后這卻是一個主觀錯誤.
對于1.10這個數(shù), 計算機只會使用近似的二進制浮點數(shù)表示, 產(chǎn)生精度影響.
從上面的例子中來看, 1,10在計算機中表示為1.099999, 這個1.10并沒有在計算機中得到精確的表示.
針對這個精度問題, 我們可能會選擇: System.out.printf("%.2f%n", 2.00 - 1.10);解決, 盡管打印出來的是正確答案, 但是依舊會暴露出一個問題: 如果精度控制在2.00 - 1.0010; 那么精度誤差依舊會出現(xiàn).
這里也說明了: 使用printf, 計算機底層依舊是使用二進制的方式來計算, 只不過這種計算提供了更好的近似值而已.
那么應(yīng)該怎么解決這個問題呢?
首先想到是使用int模擬小數(shù)每一位, 然后計算, 最后將結(jié)果又轉(zhuǎn)化為小數(shù);
以此想到的就是使用BigDecimal類, 它主要用于精確小數(shù)運算.
通過上面的代碼就能得到一個精確的值.
注: 使用BigDecimal的時候, 不要使用BigDecimal(double d)的構(gòu)造方法, 在double與double之間傳值的時候依舊會引起精度損失. 這是一個嚴(yán)重的問題.
BigDecimal底層采用的就是int[], 使用String的時候, 會將String不斷取每一位存入int[], 使用double的時候, 同理將數(shù)字的每一位存入int[], 但是double本身存在誤差, 導(dǎo)致存入的數(shù)據(jù)會出現(xiàn)誤差,例: 0.1存入double就表示為0.1000000099999999, 因此不使用double類型的構(gòu)造函數(shù)
思考:
當(dāng)然對于精確要求不高的地方, 完全可以使用float/double, 但是對于要求精度的計算, 比如貨幣 一定要使用int, long, BigDecimal.
3.長整數(shù)造成數(shù)據(jù)溢出
看下面的代碼會打印什么?
public class LongDivision{ public static void main(String args[]){ final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000; final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY); } }整個過程, 除數(shù)與被除數(shù)都是long型, 很容易保存這兩個數(shù), 結(jié)果一定是1000, 但是結(jié)果讓你失望了, 結(jié)果是5.
這又是怎么回事呢?
首先這個表達式: 24606010001000總是在int類型的基礎(chǔ)上進行計算. 即表達式是按照int的規(guī)則計算
很容易看出這個表達式計算的范圍早已超出int的取值范圍, 縱然使用long去存儲計算結(jié)果, 但是在計算的過程中就已經(jīng)出現(xiàn)計算數(shù)據(jù)溢出, 這是一個隱藏錯誤.
Java目標(biāo)確定類型的特性 => 如上例子, 不同通過 long 去確定24606010001000按照long進行存儲.
必須指定數(shù)據(jù)類型, 才能按照指定的規(guī)則進行運算.
就用前面這個例子來看, 當(dāng)指定24為24L就能防止數(shù)據(jù)計算溢出, 在進行乘法運算的時候就已經(jīng)是在long的基礎(chǔ)上進行計算.
思考:
這個問題給了我一個深刻的教訓(xùn), 當(dāng)操作數(shù)很大的時候, 要預(yù)防操作數(shù)溢出, 當(dāng)無法確定計算數(shù)會不會溢出, 所要做的就是用儲存范圍最大的類型: long 來進行計算
4.long的 “L” 與 “l(fā)” 所引發(fā)的錯誤
從上面 “長整數(shù)運算造成數(shù)據(jù)溢出” 引發(fā)又一個問題, 看下面例子:
public class Elementary{ public static void main(String[] args){ System.out.println(12345+5432l); } }乍一看, 這很簡單, 計算結(jié)果時是 6666, 但是打印的結(jié)果是 17777, 我開始頭暈了, 這很不合理.
思考過后, 發(fā)現(xiàn)了一個問題:
我把 “l(fā)” 看作是 “1”, “l(fā)” 只是用于標(biāo)識5432是一個long類型, 這個視覺上的錯誤將會引發(fā)更嚴(yán)重的問題.
思考:
小寫字母 l 與 1 很容易造成混淆, 為了避免這種錯誤, 在表示long類型數(shù)據(jù)的, 要做的就是將 “l(fā)” 換做 “L”, 掐斷產(chǎn)生混亂的源頭.
5.多重類型轉(zhuǎn)換引發(fā)的數(shù)值變化
看這樣的一個例子:
public class Multicast{ public static void main (String[] args){ System.out.println((int)(char)(byte) -1); } }看似結(jié)果是 -1, 但是運行之后, 結(jié)果變?yōu)?65535
分析一下:
byte下的-1 => 1111,1111,1111,1111,1111,1111,1111,1111 32位(4個字節(jié)) 首位1表示負(fù)號.
byte到char => 變?yōu)?0000,0000,1111,1111 16位(2個字節(jié))首位0, 就此負(fù)號變正號.
char到int => 變?yōu)?0000,0000,0000,0000,0000,0000,1111,1111 32位(4個字節(jié))
由此可見, 在byte到char的變化過程中出現(xiàn)符號轉(zhuǎn)換的問題. char首位總是0使得負(fù)號變正號
類型轉(zhuǎn)換的過程存在這樣的簡單規(guī)則:
如果最初的數(shù)值類型是有符號的,那么就執(zhí)行符號擴展;如果它是 char,那么不管它將要被轉(zhuǎn)換成什么類型,都執(zhí)行零擴展。因此這也就解釋了為什么byte到char的過程存在負(fù)號變正號.
為了在轉(zhuǎn)換的過程中保留符號, 就使用位掩碼進行限制, 例如: char c = (char)(b & 0xff); 這樣就能保證符號具有保留
思考:
在對有符號與無符號之間的轉(zhuǎn)換, 一定要注意上面的轉(zhuǎn)換規(guī)則, 如果不能確定轉(zhuǎn)換符號是否正確, 那么就避免出現(xiàn)有符號到無符號之間的轉(zhuǎn)換.
6.避免所謂聰明的編程技巧
對于交換兩個變量的內(nèi)容, 在C/C++中存在一種這樣的編程技巧:
int x = 1111; int y = 2; x^=y^=x^=y; cout<<x<<" "<<y;這樣一個簡單的連續(xù)異或就能解決變量的交換問題, 這種寫法在很久以前是為了減少臨時變量的使用, 所以這種做法在現(xiàn)在也得到了保留.
首先看這樣一個問題, 表達式x^=y, 在C/C++的編譯器中是先計算出y的值, 然后再獲取x的值, 最后再計算表達式. 但在Java中的做法是先獲得x的值, 再獲得y的值, 最后再計算.
Java的語言規(guī)范描述: 操作符的操作數(shù)是從左往右求值.
這使得在計算 x^ =y^ =x^ =y表達式中的第二個x的時候是在計算x^ =y之前的值( x的值依舊是1111 ), 并不是x^=y后的值, 這就導(dǎo)致了計算上的錯誤.
所以在Java中準(zhǔn)確的寫法是:
思考:
上面的這種寫法極其容易引起錯誤, 程序的可讀性受到很大的影響, 所以在寫代碼的時候要思考一個問題, 除非編譯器能確定操作數(shù)的運算順序, 不然不要讓編譯器去確定操作數(shù)的計算順序, 就比如這樣的表達式: x=a[i]+±a[j]++. 很容易導(dǎo)致錯誤.
7.避免使用混合運算
看如下代碼:
public class DosEquis{ public static void main(String[] args){ char x = 'X'; int i = 0; System.out.println(true ? x : 0); System.out.println(false ? i : x); } }看似將打印: XX, 但是結(jié)果卻是X88. 這是一個出乎意料的結(jié)果.
思考之后, 將可能得出這樣的結(jié)論: 出現(xiàn)這樣問題的原因是操作數(shù)的類型自動提升, char=>int.
但是又有一個問題就是為什么第一個運算不是88. 找其根本原因還是在于條件表達式的運算規(guī)則:
上面的規(guī)則決定了, 將調(diào)用哪一個print的重載函數(shù).
這種條件表達式返回值, 很容易受B, C類型影響. 當(dāng)根據(jù)返回值作條件判斷的時候, 這種性質(zhì)也將導(dǎo)致一個嚴(yán)重的問題.
思考:
上面的問題說明了, 在條件表達式中, 最后再后兩個操作數(shù)使用相同類型的操作數(shù), 以此避免返回值類型不確定的問題, 并且在其他的表達式計算中, 一定要理清楚數(shù)值之間的類型轉(zhuǎn)換.
8.發(fā)現(xiàn)隱藏的類型轉(zhuǎn)換
在這樣的表達式: x += i; 按照平常的理解, 它一定是x = x + i; 可是這樣的運算表達式是建立在x與i具有相同的類型的基礎(chǔ)上的, 如果當(dāng)x, i類型不相同的時候, 將會引發(fā)一個問題就是精度損失.
就比如:
現(xiàn)在的x不是99999, 而是-31073.
當(dāng) x += i 的時候, 出現(xiàn)的問題就是i自動轉(zhuǎn)型為short, 此時x的值就不再是99999. 而當(dāng)你將表達式寫為: x = x + i 的時候, 這是一種顯式的轉(zhuǎn)型, 自然需要強轉(zhuǎn)操作. 從而避免了隱藏的類型轉(zhuǎn)換.
思考:
復(fù)合運算會隱藏出現(xiàn)轉(zhuǎn)型操作, 這種轉(zhuǎn)型操作很有可能出現(xiàn)精度丟失.
所以在進行復(fù)合運算的時候, 避免兩邊的操作數(shù)是不同的類型, 防止編譯器出現(xiàn)危險的窄化類型, 或者不使用復(fù)合運算, 人為進行類型轉(zhuǎn)換.
9.字符串的"+"運算符
看如下代碼:
public class LastLaugh{public static void main(String[] args){ System.out.print("H"+"a"); System.out.print('H'+'a'); } }由于長期受 “+” 運算符的影響, 上面的代碼, 很容易把 ‘H’+‘a(chǎn)’ 也看作是字符串的連接, 這是一種慣性的思考方式.
在 ‘H’+‘a(chǎn)’ 表達式的運算中, 是將 ‘H’, ‘a(chǎn)’, 上升為int, 進行數(shù)值運算.
如果想讓兩個字符連接在一起, 可以采用:
思考:
在使用 “+” 運算符一定要注意操作數(shù)的類型, 以防止慣性思維導(dǎo)致的運算錯誤. 在某些場合這種錯誤有可能是致命性的.
看完字符的 “+” 運算符, 現(xiàn)在再來字符數(shù)組的 "+"運算符 :
public class A{ public static void main(String[] args){ String letters = "ABC"; char[] numbers = {'1', '2', '3'}; System.out.println(letters + " easy as " + numbers); } }上面的代碼, 最終的打印結(jié)果不是 ABC easy as 123, 而是ABC easy as [C@16f0472.
如果想到的打印結(jié)果是ABC easy as 123, 那么犯的錯誤還是上面相同的錯誤.
在打印結(jié)果的時候, 首先會進行字符串連接, 當(dāng) “easy as” 這個字符串連接 char[] 的時候, 那么調(diào)用的是char[] 的toString(), 而系統(tǒng)并沒有重寫toString(), 所以最后調(diào)用的就是Object的toString();
為了修正這樣的錯誤, 給出如下解決方式:
而在C/C++中, char numbers[4] = {‘1’, ‘2’, ‘3’, ‘\0’ }; 代表的就是一個字符串.
思考:
牢記, 數(shù)組類型的toString都沒有重寫, 如果想獲得數(shù)組中的值, 避免調(diào)用數(shù)組類型的toString, 或者讓系統(tǒng)隱藏調(diào)用, 而是直接遍歷數(shù)組獲得其中的值.
10."=="運算符進行比較
- 問題1:
這里先說明第一個問題, 就是Java中的 “==” 運算符: 在比較基本類型的時候, 是比較基本類型值的關(guān)系; 在比較數(shù)組, 或者對象的時候是比較對象之間的引用值關(guān)系. 但是這里要注意的是:
在比較Integer, Long(本人親測)這兩種的時候, 比較-128~127的時候是從緩存池中拿取數(shù)據(jù).
思考:
這里我想表達的意思就是, 如果要進行對象內(nèi)容之間的比較, 務(wù)必重寫equals, 然后使用equals. 還有避免在基本類型與包裝類型混合狀態(tài)的基礎(chǔ)上使用 “==”, 就比如 Integer. 這個很容易導(dǎo)致錯誤.
- 問題2
當(dāng)看到這樣的代碼的時候:
我想去比較pig與dog引用值關(guān)系, pig 與 dog 的引用值肯定是相同的, 但是最后的輸出結(jié)果卻是false. 在這里忽略了一個問題, 那就是前面的 “+” 的運算級比 “==” 的運算級高, 看似是比較pig與dog的引用值, 最后卻是比較"Animals are equal: length: 10"與dog的引用值關(guān)系.
現(xiàn)在給出下面的修正方案:
思考:
從這里也看出, 比較對象內(nèi)容的時候, 務(wù)必使用已經(jīng)重載后equals, 除非刻意比較兩個對象的引用值, 否則千萬別使用"==".
上面有錯, 還請指出, 如果認(rèn)為我寫的還不錯, 還請點個贊, 多多支持一下, O(∩_∩)O~~
總結(jié)
以上是生活随笔為你收集整理的学习Java中遇到的问题积累_1的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++关联容器总结一
- 下一篇: revit如何根据坐标进行画线_在工程设