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.8999999999999999
4 } 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.90
7 } 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);//X
6 System.out.println(false?i:x);//88
7 } 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);//88
11 System.out.println(true?x:l);//88 12 //對float類型的常量進行測試
13 double d=12.0; 14 System.out.println(true?x:12.0);//88.0
15 System.out.println(true?x:d);//88.0
16 } 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 short
8 } 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);//false
13 System.out.println(str1==str3);//true
14 } 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);//e
6 } 7 }