Java解惑精鍊版(一)
1、找零時刻(貨幣計算問題)
問題簡述:Tom現有$2.0,購買了$1.10美元的貨物,店主應該找他多少零錢?
1 public class Change {2 public static void main(String[] args) {3 System.out.println(2.00-1.10);//0.89999999999999994 }5 }
運行結果:0.8999999999999999
問題在於1.10這個數字不能被精確表示成為一個double,因為它被表示成為最接近它的double值。更一般地說,問題在於並不是所有的小數都可以用二進位浮點數來精確表示的。
解決該問題的一種方式是使用某種整數類型,例如int或long,並且以分為單位來執行計算。如果採納了此路線,請確保該整數類型大到足夠表示在程式中你將要用的所有值。
System.out.println((200-110)+"cents");
解決該問題的另一種方式是使用執行精確小數運算的BigDecimal。告誡:一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)構造器。BigDecimal:不可變的、任意精度的有符號十進位數。BigDecimal表示的數值是(unscaledValue × 10-scale)。
1 import java.math.BigDecimal;2 3 public class Change {4 public static void main(String[] args) {5 System.out.println(new BigDecimal("2.00")6 .subtract(new BigDecimal("1.10")));//0.907 }8 }
總結:在需要精確答案的地方,要避免使用float和double;對於貨幣計算,要使用int、long或BigDecimal。使用BigDecimal的計算很有可能比那些原始類型的計算要慢一些,對某些大量使用小數計算的程式來說,這可能會成為問題,對於大多數程式來說,這顯得一點也不重要。
2、條件運算子
先看如下程式碼:
1 public class DosEquis {2 public static void main(String[] args) {3 char x='X';4 int i=0;5 System.out.println(true?x:0);//X6 System.out.println(false?i:x);//887 }8 }
運行結果: X 88
運行結果是:X88,而不是XX,這是為什麼呢?
下面看條件運算式結果類型的規範中的核心三點:
1)如果第二個和第三個運算元具有相同的類型,那麼它就是條件運算式的類型。換句話說,你可以通過繞過混合類型的計算來避免大麻煩。
2)果一個運算元的類型是T,T表示byte、short或char,而另一個運算元是一個int類型的常量運算式,它的值是可以用類型T表示的,那麼條件運算式的類型就是T。
3)否則,將對運算元類型運用位元字提升,而條件運算式的類型就是第二個和第三個運算元被提升之後的類型。
2、3兩點對本謎題是關鍵。在程式的兩個條件運算式中,一個運算元的類型是char,另一個的類型是int。在兩個運算式中,int運算元都是0,它可以被表示成一個char。然而,只有第一個運算式中的int運算元是常量(0),而第二個運算式中的int運算元是變數(i)。因此,第2點被應用到了第一 個運算式上,它返回的類型是char,而第3點被應用到了第二個運算式上,其返回的類型是對int和char運用了位元字提升之後的類型,即 int。
示範樣本擴充:
1 public class DosEquis { 2 public static void main(String[] args) { 3 char x='X'; 4 //對int類型的常量進行測試 5 int i=12; 6 System.out.println(true?x:12);//X 7 System.out.println(true?x:i);//88 8 //對long類型的常量進行測試 9 long l=12L;10 System.out.println(true?x:12L);//8811 System.out.println(true?x:l);//8812 //對float類型的常量進行測試13 double d=12.0;14 System.out.println(true?x:12.0);//88.015 System.out.println(true?x:d);//88.016 }17 }
3、賦值運算子(複合賦值運算式)
半斤:
我們給出一個對變數x和i的聲明即可,它肯定是一個合法的語句:
x += i;
但是,它並不是:
x = x + i;
許多程式員都會認為運算式(x=x+i;)是運算式(x+=il;)的簡寫形式。但是這並不十分準確。這兩個運算式都是賦值運算式,第二天語句使用的是簡單賦值操作符(=),而第一條語句使用的是複合賦值操作符。(複合賦值操作符包括:+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和!=).Java語言規範中講到,複合賦值 E1 op= E2; 等價於簡單賦值 E1 = (T) ((E1) op (E2)); ,其中T是E1的類型,除非E1隻被計算一次。
換句話說,複合賦值運算式自動地將它們所執行的計算的結果轉型為其左側變數的類型。如果結果的類型與該變數的類型相同,那麼這個類型不會造成任何影響。然而,如果結果的類型比該變數的類型要寬,那麼複合賦值操作符將隱形的執行強制類型轉換。因此,我們有很好的理由去解釋為什麼在嘗試著執行等價的簡單賦值可能會產生一個編譯錯誤。
程式碼範例示範:
1 public class Demo {2 public static void main(String[] args) {3 short x=0;4 int i=123456;5 x += i;//複合賦值編譯將不會產生任何錯誤:包含了一個隱藏的轉型!6 7 //x=x+i;//編譯錯誤:Type mismatch:cannot convert from int to short8 }9 }
為了避免複合賦值運算式隱形的進行類型轉換(會就是精度),請不要講複合賦值操作符作用於byte、short或char類型的變數上。在將複合賦值操作符作用於int類型的變數上時,要確保運算式右側不是long、float或double類型。在將複合賦值操作符作用於float類型的變數上時,要確保運算式右側不是double類型。
八兩:
與上面的例子相反,如果我們給出的關於變數x和i的聲明是如下的合法語句:
x = x + i;
但是,它並不是:
x += i;
乍一看,這個謎題可能看起來與前面一個謎題相同。但是請放心,它們並不一樣。這兩個謎題在哪一條語句必是合法的,以及哪一條語句必是不合法的方面,正好相反。
就像前面的謎題一樣,這個謎題也依賴於有關複合賦值操作符的規範中的細節。二者的相似之處就此打住。基於前面的謎題,你可能會想:複合賦值操作符比簡單賦值操作符的限制要少一些。在一般情況下,這是對的,但是有這麼一個領域,在其中簡單賦值操作符會顯得更寬鬆一些。
複合賦值操作符要求兩個運算元都是原始類型的,例如int,或封裝了的原始類型,例如Integer,但是有一個例外:如果在+=操作符左側的運算元是String類型的,那麼它允許右側的運算元是任意類型,在這種情況下,該操作符執行的是字串串連操作。簡單賦值操作符(=)允許其左側的是對象參考型別,這就顯得要寬鬆許多了:你可以使用它們來表示任何你想要表示的內容,只要運算式的右側與左側的變數是賦值相容的即可。
程式碼範例示範:
1 public class Demo { 2 public static void main(String[] args) { 3 Object o="Buy"; 4 String s="Effective Java!"; 5 //簡單賦值是合法的,因為o+s是String類型的,而String類型又是與Object賦值相容的: 6 o=o+s; 7 //複合賦值是非法的,因為左側是一個Object參考型別,而右側是一個String類型: 8 //o+=s; 9 //編譯錯誤:The operator += is undefined for the argument type(s)Object,String 10 }11 }
4、String “+” 串連操作:畜牧場
George Orwell的《畜牧場(Animal Farm)》一書的讀者可能還記得老上校的宣言:“所有的動物都是平等的。”下面的Java程式試圖要測試這項宣言。那麼,它將列印出什麼呢?
1 public class Demo { 2 public static void main(String[] args) { 3 final String pig = "length: 10"; 4 final String dog = "length: " + pig.length(); 5 System.out.println("pig=:"+pig+",dog="+dog); 6 //pig=:length: 10,dog=length: 10 7 System.out. println("Animals are equal: "+ pig == dog);//false 8 9 String str1="length: 10";10 String str2="length: " + str1.length();11 String str3="length: 10";12 System.out. println(str1==str2);//false13 System.out.println(str1==str3);//true14 }15 }
對該程式的表面分析可能會認為它應該列印出Animal are equal: true。畢竟,pig和dog都是final的string類型變數,它們都被初始化為字元序列“length: 10”。換句話說,被pig和dog引用的字串是且永遠是彼此相等的。然而,==操作符測試的是這兩個對象引用是否正好引用到了相同的對象上。在本例中,它們並非引用到了相同的對象上。
String類型的編譯期常量是記憶體限定的。換句話說,任何兩個String類型的常量運算式,如果標明的是相同的字元序列,那麼它們就用相同的對象引用來表示。如果用常量運算式來初始化pig和dog,那麼它們確實會指向相同的對象,但是dog並不是用常量運算式初始化的。既然語言已經對在常量運算式中允許出現的操作作出了限制,而方法調用又不在其中,那麼,這個程式就應該列印Animal are equal: false,對嗎?
實際上不對。如果你運行該程式,你就會發現它列印的只是false,並沒有其它的任何東西。它沒有列印Animal are equal: 。它怎麼會不列印這個字串字面常量呢?畢竟列印它才是正確的呀!因為:+ 操作符,不論是用作加法還是字串串連操作,它都比 == 操作符的優先順序高。因此,println方法的參數是按照下面的方式計算的:
System.out.println(("Animals are equal: " + pig) == dog);
這個布林運算式的值當然是false,它正是該程式的所列印的輸出。
令人暈頭轉向的Hello
請看下面的程式:
1 /** 2 * Generated by the IBM IDL-to-Java compiler, version 1.0 3 * from F:\TestRoot\apps\a1\units\include\PolicyHome.idl 4 * Wednesday, June 17, 1998 6:44:40 o’clock AM GMT+00:00 5 */ 6 public class Demo { 7 public static void main(String[] args) { 8 System.out.print("Hell"); 9 System.out.println("o world!");10 }11 }
這個謎題看起來相當簡單。該程式包含了兩條語句,第一條列印Hell,而第二條在同一行列印o world,從而將兩個字串有效地串連在了一起。因此,你可能期望該程式列印出Hello world。但是很可惜,你犯了錯,實際上,它根本就通不過編譯。
問題在於注釋的第三行,它包含了字元\units。這些字元以反斜線(\)以及緊跟著的字母u開頭的,而它(\u)表示的是一個Unicode逸出字元的開始。遺憾的是,這些字元後面沒有緊跟四個十六進位的數字,因此,這個Unicode逸出字元是病構的,而編譯器則被要求拒絕該程式。Unicode逸出字元必須是良構的,即使是出現在注釋中也是如此。
1 public class Demo {2 public static void main(String[] args) {3 //\\u後面必須跟4個16進位的數字,否則會出現編譯錯誤4 char ch='\u0065';5 System.out.println(ch);//e6 }7 }