這一節會更深入地探討表達式。回憶一下,表達式是一段用來表示或計算值的代碼。表達式可以是文字、變量、函數調用或者這些的組合,由 +、> 這樣的運算符連接到一起。表達式的值可以賦給變量,在子程序調用中用作參數,或者與其它的值組合成為更復雜的表達式。(這些值在某些情況下甚至可以被忽略掉,如果你想要這么干;這比你想象中更加常見)。表達式是編程的基礎。到目前為止,本書都只是順帶提到了表達式。這一節會告訴你一個完整的故事(這里會忽略一些不常見的運算符)。
表達式的基礎由文字(比如674、3.14、true 和 ‘X’)、變量和函數調用組成。還記得函數是一個帶返回值的子程序。你可能已經看到過一些函數的例子,比如 TextIO 類的輸入程序以及 Math 類的數學計算函數。
Math 類還包含了一組數學常量,在數學表達式中非常有用:Math.PI 表示 π (圓周率)、 Math.E 表示 e(自然對數的基數)。這些“常量”實際上都是 Math 類中 double 類型的成員變量。它們是數學常量的近似值,實際的精確值要求無限長度的數字。標準的 Integer 類包含了一組與 int 數據類型相關的常量:Integer.MAX_VALUE 是最大的 int,2147483647;Integer.MIN_VALUE 是最小的 int,-2147483648。類似地,Double 類包含了一些與 double 類型相關的常量。Double.MAX_VALUE 是最大的 double 值,而 Double.MIN_VALUE 是最小的正值。Double 還包含了表示無限的數值,Double.POSITIVE_INFINITY 和 Double.NEGATIVE_INFINITY。而特殊的 Double.NaN 表示未定義值。例如,Math.sqrt(-1) 的結果就是 Double.NaN。
文字、變量和函數調用都是簡單表達式。通過運算符可以將簡單表達式組合成復雜表達式。運算符包括 + 將兩個數值相加,> 比較兩個值大小,等等。當表達式中包含了若干運算符時,就會出現優先順序問題,它決定了運算符在計算式如何分組。例如,在表達式 “A + B * C” 中,B*C 會先計算,然后結果再與 A 相加。我們說,乘法 (*) 的優先級比加法 (+) 高。如果默認的優先級順序不是你想要的,那么可以使用括號明確指定你期望的分組。例如,你可以使用”(A + B) * C” 表明希望先將 A 與 B 相加再乘以 C。
這一節的后面會對Java中的運算符細節進行詳細地解釋。Java提供了很多運算符,我不會每個都進行介紹,但是這里會給出大多數最重要的運算符說明。
算數運算符包括加法、減法、乘法和除法。它們的符號分別是 +、-、* 和 /。這些操作可以用于任意類型的數值:byte、short、int、long、float或double。(它們還可以用在 char 類型上,在這種情況下 char 被當做 integer 使用;char 會被轉換成它的 Unicode 代碼,并代入算數運算符操作。)當計算機實際計算時,計算中的兩個值必須是相同類型。如果你的程序告訴計算機輸入了兩個不同類型的值,那么計算機會將其中一個轉換成另一個的類型。例如,計算 37.4 + 10 這個表達式時,計算機會將整數 10 轉換成實數 10.0,然后計算 37.4 + 10.0。這被稱為類型轉換。通常,你不必關心表達式中的類型轉換,因為計算機會替你自動完成。
兩個數值(如果必要,會對其中一個進行類型轉換)計算后的結果與其類型一致。如果兩個 int 相乘,結果是 int;兩個 double 相乘,結果是 double,這是可預見的結果。但是,當你使用 / 時必須非常小心。如果兩個 int 相除,結果是 int;如果商是分數,會被舍去。例如,7/2 結果是 3,而不是 3.5。假設N是整型變量,那么 N/100 結果不是整數,并且當 N 大于 1 時 1/N 等于 0!這是很多常見編程錯誤的根源。可以把其中一個運算符改為實數,強迫計算機輸出實數:比如,當計算機處理 1.0/N 時,首先把N轉成實數,從而與 1.0 的類型匹配,這樣得到的結果就是實數。
Java還提供了計算除法操作的余數。計算余數的運算符為 %。如果 A 和 B 都是整數,那么 A % B 表示 A 除以 B 的余數。(然而,對于負數Java中的 % 與數學計算中的“取模”操作不同。在Java中,如果 A 或 B 為負數,那么 A % B 的結果也是負數)。例如,7 % 2 等于 1,34577 % 100 等于 77,50 % 8 等于 2。% 常被用來測試給定整數是奇數還是偶數:如果 N % 2 等于0,那么N是偶數;如果 N % 2 等于 1,那么N是奇數。一般來說,你可以通過 N % M 結果是否為 0,判斷整數N是否可以被M取模。
% 運算符也適用于實數。通常,A % B 表示從 A 中移除多個 B 后遺留的數值。例如,7.52 % 0.5 等于 0.02。
最后,你還可能需要一元減法運算符,得到一個數的負數。例如,-X 等價于 (-1)*X。出于完備性考慮,Java還提供了一元加法運算符,例如 +X。景觀在實際中沒有任何作用。
順便說一下,+ 操作還可以用來向 String 字符串連接任意類型的值。當你使用 + 連接字符串時,這是另一種形式的類型轉換,任意的對象都會自動轉換為 String。
你會發現,為變量加 1 是編程中極其常見的操作,為變量減 1 也一樣。可以像下面這樣賦值為變量加 1:
x = x + 1 語句的結果是,用變量 x 原來的值加 1 后再賦值給變量 x。也可以用 x++ 得到相同的效果(或者你可能會喜歡寫成 ++x)。實際上,這么寫會改變 x 的值,得到的效果與 “x = x + 1″ 一樣。上面的兩個語句可以改為:
類似地,你也可以寫 x–(或 –x)從 x 中減1。也就是說,x– 與 x = x - 1 執行了相同的計算。向變量加 1 稱為變量遞增,從變量減 1 稱為變量遞減。運算符 ++ 和 — 分別被稱為遞增運算符和遞減運算符。這些運算符可以用于任何數值類型的變量,以及char類型的變量( ‘A’++ 結果是 ‘B’)。
通常,運算符 ++ 和 — 用在語句中,比如 “x++;” 或 “x–;”。這些語句是改變 x 值的指令。然而,將x++、++x、x–或–x作為表達式或表達式的一部分也是合法的。也就是說,你可寫出下面的代碼:
“y = x++;”的效果是 x 變量加1,然后把某個值賦給 y。賦給 y 的值是表達式 x 加 1 之前的值。因此,假設 x 等于 6,那么 “y = x++;” 執行后,會將 x 變為 7,但是 y 的值被賦為 6。因為賦給 y 的是 x 加 1 前的舊值。而另一種寫法,++x 會得到加1后的新值。所以,還是假設 x 等于 6, ”y = ++x;” 會把 x 和 y 都變為 7。運算符 — 也是類似的用法。
特別要注意,x = x++; 這個語句沒有改變 x 的值!這是因為賦給 x 的是 x 的舊值,即在語句執行前 x 的值。最終結果是,x 增加了 1,但是馬上被改回了原來的值!你還需要記住,x++ 不等同 于 x + 1。表達式 x++ 改變了 x 的值,而 x + 1 沒有改變。
這里會讓你感到困惑,我從學生程序中看到很多由此造成的bug。我的建議是:不要寫這種讓人困惑的代碼。++ 和 — 只在單獨的語句使用,不要用成表達式。在接下來的示例中,我會遵循這條建議。
Java提供了布爾比例和布爾表達式表示條件,條件結果可以為 true 或 false。組織布爾表達式可以通過關系運算符比較兩個值。
這些運算符可以比較任意數值類型的數值。還可以用來比較char類型的值。對字符串來說,< 和="">被定義為根據字符的 Unicode值 進行比較(結果并不是完全按照字母順序比較,這可能不是你想要的。所有大寫字母小于小寫字母。)
使用布爾表達式時你應當記住,對計算機而言,布爾值并沒有什么特殊的地方。在下一章中,你會看到如何在循環和分支中使用它們。你可以像賦值給數字變量一樣,給布爾變量賦布爾值。函數會返回布爾值。
順帶說一下,運算符 == 和 != 也可以用來比較布爾值。在某些情況下這是非常有用的。例如,你可以看下面這段代碼:
關系運算符 <、>、<= 和="">= 不能比較String值。你可以合法地使用 == 和 != 來比較字符串,但是由于對象行為的差別,可能不會得到你期望的結果。(== 運算符可以檢查兩個對象的內存地址是否相同,而不是判斷對象中值是否相等。對某些對象,在某種情況下,你可能想要做類似的檢查——但字符串不行。下一章我會再討論這個話題。)相反地,你要使用 equals()、equalsIgnoreCase() 和 compareTo(),這些在2.3.3章節中進行了討論,如何比較兩個字符串。
另一個 == 和 != 不起作用的地方是與 Double.NaN 比較。這個常量表示 double 類型的未定義值。無論 x 的值是否為 Double.NaN,x == Double.NaN 和 x != Double.NaN 都會返回 false!要檢測實數類型的值 x 是否為 Double.NaN,可以使用函數 Double.isNaN(x) 返回判斷結果。
在英語中,復雜條件通過 and、or 和 not 組合在一起。例如,“If there is a test and you did not study for it…”,and、or 和 not 都是布爾運算符,在Java中也同樣存在。
在Java中,布爾運算符“and”表示為 &&。&& 運算符用來結合 2 個布爾值。結果還是 1 個布爾值。如果兩個值都是 true,那么結果為 true;如果其中一個為 false,那么結果為 false。例如,如果 x 等于 0 并且 y 等于 0,“(x == 0) && (y == 0)” 結果為true。
布爾運算符“or”在Java中表示為 ||。(由兩個垂直的行字符 | 組成。)如果 A 或 B 其中一個為 true 或者都為 true,那么表達式 “A || B” 結果為 true。只有 A 和 B 同時為 false 時,“A || B” 結果為 false。
運算符 && 和 || 被稱為短路版本的布爾運算符。也就是說,&& 或 || 的第二個操作符不一定會計算。考慮下面這個測試
假設 x 的值實際為 0,在這種情況下,y/x 是未定義的觸發運算(除0)。然而,計算機永遠不會執行這個觸發,因為當計算機對 (x != 0) 計算結果時,發現結果為 false。這時計算機知道 ((x != 0) && 任意表達式) 一定會為 false。因此,它不會再計算對第二個運算符求值。運算被短路,從而避免了除0的情況。(這個聽起來有點偏技術性,事實也是如此。但有時候,會讓編程生活更輕松些。)
布爾運算符“not”是一元運算符。在Java中用 ! 表示,寫在單個運算對象的前面。例如,假設 test 是一個布爾變量,那么
將會對 test 的值取反,從 true 變為 false 或者從 false 變為 true。
任何優秀的編程語言都有一些漂亮的小功能。雖然不是必須的功能,但可以讓你在使用時感覺很酷。Java也有,條件運算符中的三元運算符。它有 3 個操作數,有 2 個部分:,? 和 : 組合在一起。三元運算符形式如下:
計算機會檢測布爾表達式的值。如果值為 true,會計算 expression1,否則計算 expression2。例如:
如果N是偶數,會把 N/2 賦給 next(即 N % 2 == 0 為 true);如果N是基數,會把 (3*N+1) 賦給 next(這里的括號不是必須的,但是會讓表達式更容易理解)。
你可能已經對賦值表達式非常熟悉,使用 “=” 將表達式賦值給變量。實際上,在某種意義上 = 也是運算符,可以將它用作表達式或者作為復雜表達式一部分。表達式 A=B 與向 A 賦值的語句作用相同。因此,如果你想要把 B 的值賦給 A,同時判斷值是否為 0,可以這么寫:
通常,我會強調不要那么做!
通常,表達式中右邊的類型必須和左邊一致。然而,在某些情況下,計算機會對表達式的值自動轉換,以匹配變量的類型。比如在數值類型 byte、short、int、ong、float、double 中,列表中靠前的類型數值可以自動轉換為列表中靠后的類型。
在不影響語義的情況下,轉換應當自動進行。任何int應當可以被轉換為數值相同的 double 類型。然而,int 值中有一些超過了short 類型的合法范圍。比如不能將 100000 轉為 short,因為 short 的最大值是 32767.
在某些情況下,比如在不能自動轉換的情況下你可能想要進行強制轉換。這里,你需要使用類型轉換。類型轉換可以把類型名放在括號里,放在你需要轉換的數值前面。例如:
你可以將任何數值類型轉換為其他數值類型。然而,你應當注意,轉換過程中可能會改變數值。例如,(short)100000 等于 -31072。(-31072 是通過 100000 丟掉2個字節,保留 4 個字節得到的 short 值——轉換中會丟失 2 個字節的信息。)
當你將實數轉為整型時,小數部分被丟掉了。例如,(int)7.9453 等于 7。另一個類型轉換的例子,從 1 到 6 范圍中得到隨機整數。函數 Math.random() 會返回 0.0 到 0.9999… 之間的實數,因此 6*Math.random() 的結果在 0.0 和 5.999… 之間。類型轉換操作符 (int) 可以用來將結果轉為整形:(int)(6*Math.random())。因此,(int)(6*Math.random()) 結果會得到 0、1、2、3、4、5 之間的某個整數。要得到 1 到 6 之間的隨機數,可以加 1:”(int)(6*Math.random()) + 1″。(6*Math.random() 周圍的括號是必須的,因為用括號來保證運算的優先順序;如果沒有括號,類型轉換運算符只能對 6 起效)。
char 類型與整型幾乎等價。你可以將 char 類型賦值給任意整型變量。你還可以將 0 到 65535 內的常量賦值給 char 變量。你還可以顯示地在 char 和數值型之間進行類型轉換,比如 (char)97 得到 ‘a’,(int)’+’ 是 43,(char)(‘A’ + 2) 等于 ‘C’。
String 和其它類型之間的不能進行類型轉換。任意類型轉換成字符串的一種方法,可以將他們與一個空字符串連接。例如,”' + 42 可以得到字符串 “42″。但是還有更好的辦法,使用 String 類中的靜態方法 String.valueOf(x)。String.valueOf(x) 返回輸入 x 轉換得到的 String。例如,String.valueOf(42) 返回字符串 “42″。而且,如果 ch 是一個 char 變量,那么 String.valueOf(ch) 返回長度為 1 的字符串,字符串中內容是 ch 變量中的唯一一個字符。
也可以將特定字符串轉換為其它類型的值。例如,字符串”10″ 可以被轉轉換為整型值 10,而字符串 “17.42e-2″ 可以轉換為 double 值 0.1742。在Java中,這些轉換可以由內建方法完成。
標準的 Integer類 提供了靜態成員函數,可以將 String 轉為 int。特別地,如果 str 是任意的 String 表達式,Integer.parseInt(str) 會試著將str的值轉為int類型。例如 Integer.parseInt(“10″) 得到 int 值 10。如果 Integer.parseInt 傳入的參數是非法的 int 值,那么會返回錯誤。
類似地,標準的 Double 類提供了 Double.parseDouble方法。如果 str 是 String 類型,調用 Double.parseDouble(str) 方法會試圖將 str 轉為 double 類型的值。當 str 表示的是非法 double 值,就會返回錯誤。
讓我們會到賦值語句。Java有許多賦值運算符變種,用來保存類型。例如,”A += B” 等價于 “A = A + B”。除了關系運算符,每個Java運算符都可以處理 2 個操作數,這樣就得到了 1 個類似的賦值運算符。例如:
組合式賦值運算符 += 甚至可以用于String。回憶一下,+ 運算符可以將 string 作為其中的一個操作數,表示連接操作。既然 str += x 等價于 str = str + x,那么當 += 將 string 作為操作數時,這個操作就把右邊的操作數附到字符串的結尾。例如,如果 str 的值是 “tire”,那么語句 str += ’d'; 就會返回 str 值為 “tired”。
如果在表達式中使用了多個運算符,并且沒有使用括號來顯示地指定計算順序,那么你就需要考慮優先規則來確定實際的計算順序。(建議:不要讓你自己和程序的閱讀者產生困惑,大方地使用括號吧。)
下面是本章討論過的運算符列表,按照優先級從高(第一個計算)到低(最后計算)順序排列:
同一行的運算符優先級相等。在沒有括號的情況下,優先級相同的運算符串在一起,一元運算符和賦值運算符的計算順序是從右到左,而剩下的其它運算符計算順序是從左到右。例如 A*B/C 表示 (A*B)/C,而 A=B=C 表示 A=(B=C)。(考慮到 B=C 作為表達式的同時也進行了給 B 賦值的運算,你能看出A=B=C表達式是如何計算的嗎?)