第 4 章 運算式 (Expression)
本章提要 4-1 甚麼是運算式? 4-2 指定運算子 (Assignment Operator) 4-3 數值運算 4-4 布林運算 (Logical Operation) 4-5 位元運算 (Bitwise Operation) 4-6 運算式的運算順序 4-7 資料的轉型 (Type Conversion) 4-8 其他運算子
4-1 甚麼是運算式? 在 Java 程式語言中, 大部分的敘述都是由運算式 (Expression) 所構成。所謂的運算式, 則是由一組一組的運算子 (Operator) 與運算元 (Operand) 所構成。其中, 運算子代表的是運算的種類, 而運算元則是要運算的資料。舉例來說:
甚麼是運算式? 就是一個運算式, 其中 + 就是運算子, 代表要進行加法運算, 而要相加的則是 5 與 3 這兩個資料, 所以 5 與 3 就是運算元。 要注意的是, 不同的運算子所需的運算元數量不同, 像是剛剛所提的加法, 就需要二個運算元, 這種運算子稱為二元運算子 (Binary Operator);如果運算子只需單一個運算元, 就稱為單元運算子 (Unary Operator)。
甚麼是運算式? 另外, 運算元除了可以是字面常數以外, 也可以是變數, 例如: 甚至於運算元也可以是另外一個運算式, 例如:
甚麼是運算式? 實際在執行時, Java 會將 5 與 3 * 4 視為是加法的兩個運算元, 其中 3 * 4 本身就是一個運算式。 每一個運算式都有一個運算結果, 以加法運算來說, 兩個運算元相加的結果就是加法運算式的運算結果。當某個運算元為一個運算式時, 該運算元的值就是這個運算式的運算結果。
甚麼是運算式? 以剛剛的例子來說, 3 * 4 的結果 12 就是 3 * 4 這個運算式的運算結果, 它就會作為前面加法運算的第二個運算元的值, 相當於將原本的運算式改寫為 5 + 12 了。 另外, 在運算式當中, 也可以如同數學課程中所學的一樣, 任意使用配對的小括號 "( )", 明確表示計算的方式, 舉例來說:
甚麼是運算式?
甚麼是運算式? 其中第 4 與第 5 行的運算式因為加上了括號, 所以兩個運算式的順序並不相同, 最後的結果也不一樣。 以下就分門別類, 介紹 Java 程式語言中的運算子。
運算子的語法 在以下的章節中, 我們會在說明每一個運算子之前, 列出該運算子的語法, 舉例來說, 指定運算子的語法就是: 這個意思就表示要使用指定運算子 (=) 的話, 必須有 2 個運算元, 左邊的運算元一定要是一個變數 (以 var 表示) , 右邊的運算元則沒有限制。
運算子的語法 注意到如果某個 運算子的運算元 必須受限於某種 型別的話, 會以 右表的單字來表 示: 否則僅以 opr 來表示該位置需要 1 個運算元。
運算子的語法 另外, 我們也會以數字字尾區別同類型的不同運算元, 比如說在乘法運算子中, 語法就是: 就表示需要 2 個數值型別的運算元。
4-2 指定運算子 (Assignment Operator) 指定運算子是用來設定變數的內容, 它需要 2 個運算元, 左邊的運算元必須是一個變數, 而右邊的運算元可以是變數、字面常數或是運算式。這個運算子的作用如下:
指定運算子 (Assignment Operator) 如果右邊的運算元是一個運算式, 那麼指定運算子的作用就是把右邊運算式的運算結果放入左邊的變數。 如果右邊的運算元是一個變數, 那麼指定運算子就會把右邊變數的內容取出, 放入左邊的變數。 如果右邊的運算元是個字面常數, 就直接將常數值放入左邊的變數。
指定運算子 (Assignment Operator) 請看以下的範例:
指定運算子 (Assignment Operator) 其中, 第 3 行是直接使用字面常數設定變數的值;第 4 行則是使用指定運算子將右邊運算式的運算結果放入左邊的變數 i 中;而第 5 行就是使用指定運算子將右邊變數 i 的內容放到左邊的變數 j 中, 因此, 最後的結果就使得 i 與 j 這兩個變數的內容一模一樣了。
當成運算元的指定運算式 前面提過, 每一個運算式都有一個運算結果, 而指定運算式的運算結果就是放入指定運算子左邊變數的內容。因此, 我們可以運用之前所說, 運算式也可以是某個運算式的運算元, 將指定運算式作為運算元使用。舉例來說:
當成運算元的指定運算式
當成運算元的指定運算式 在第 4 行中, 就使用了 j = 3 這個指定運算式來作為加法的其中一個運算元, 因此, 這一行的執行過程就像是這樣:
當成運算元的指定運算式 先將 3 放到變數 j 中, 所以 j 的內容變成 3, 而 j = 3 這個運算式的運算結果也是 3。 將 (j = 3) + 5 這個運算式的結果 (也就是 8) 放入變數 i 中, 所以 i 的內容就變成 8 了。
同時指定給多個變數 依此類推, 您也可以將同樣的內容同時設定給 2 個以上的變數:
同時指定給多個變數 第 4 行的指定運算會將 3 + 5 這個運算式的運算結果 (8) 放入變數 l 中, 而l = 3 + 5 這個運算式的運算結果 (一樣是 8) 放入 k 中, 因此 l 與 k 的內容就都是 8。 依此類推, 最後 i 、j 、k 、l 這 4 個變數的內容就全部都是 8 了。
同時指定給多個變數
4-3 數值運算 四則運算 遞增與遞減運算 單運算元的正、負號運算子
四則運算 在數值運算中, 最直覺的就是四則運算, 不過在 Java 中的四則運算中乘法是以 * 表示, 而除法則是以 / 表示, 例如:
四則運算
四則運算 要特別注意的是, 由於 i 與 j 都是 int 型別, 因此在進行除法時, 計算的結果是整數, 當無法整除時, 所得到的就會是商數。
四則運算 您可以透過 % 運算子 (Remainder Operator), 來取得餘數:
四則運算 如果有任何一個運算元是浮點數, 那麼除法的結果就會是浮點數:
四則運算
遞增與遞減運算 由於在設計程式的時候, 經常會需要將變數的內容遞增或是遞減, 因此Java 也設計了簡單的運算子, 可以用來代替使用加法運算子或減法運算子來幫變數加 1 或是減 1 的敘述。
遞增與遞減運算 如果您需要幫變數加 1, 可以使用 ++ 這個遞增運算子 (Increment Operator) ;如果需要幫變數減 1, 則可以使用 -- 這個遞減運算子 (Decrement Operator) :
遞增與遞減運算
遞增與遞減運算 在第 4 行使用了遞增運算子, 因此變數 i 的內容會變成 5 + 1, 也就是 6。而在第 6 行中, 使用了遞減運算子, 因此變數 i 就又變回 5 了。
遞增與遞減運算 要注意的是, 遞增或是遞減運算子可以寫在變數的後面, 也可以寫在變數的前面, 但其所代表的意義並不相同, 請看這個範例:
遞增與遞減運算
遞增與遞減運算 我們分別在第 3 、8 行將 i 的內容設定為 5, 然後在第 4 與第 9 行的運算式中使用遞增運算子設定變數 j 的內容。這 2 行程式唯一的差別就是遞增運算子的位置一個在變數後面、一個在變數前面, 結果卻不相同。 主要的原因就是當遞增運算子放在變數後面時, 雖然會遞增變數的值, 但遞增運算式的運算結果卻是變數尚未遞增前的原始值。
遞增與遞減運算 因此, 第 4 行的運算式相當於以下此程式: 這種方式稱為後置遞增運算子 (Post Increment Operator)。如果把遞增運算子擺在變數之前, 那麼遞增運算式的運算結果就會是變數遞增後的內容。因此, 第 9 行的敘述就相當於以下這行程式:
遞增與遞減運算 由於遞增運算式的運算結果是變數遞增後的值, 所以 ++i 先變成 6 之後才和 5 相加, 設定給 j, 因此 j 就變成 11 了。這種方式稱之為前置遞增運算子 (Prefix Increment Operator)。 請再看以下的範例, 會更清楚遞增運算的方式:
遞增與遞減運算
遞增與遞減運算 其中第 4 行的動作可以拆解成以下步驟:
遞增與遞減運算 由於是後置遞增運算, 因此其中 i++ 的運算結果是變數 i 未遞增前的值 5, 變成
遞增與遞減運算 所以最後 j 變成 16。 而第 9 行則是前置遞增運算, 所以遞增運算式的運算結果就等於遞增後變數 i 的值 6, 因此這行程式就等於是: 所以最後 j 就變成 17 了。
遞增與遞減運算 要特別提醒的是, 遞增與遞減運算子只能用在變數上, 也就是說, 您不能撰寫這樣的程式: 另外, 遞增或是遞減運算也可使用在浮點數值型別的變數, 而非只能用在整數變數上。
單運算元的正、負號運算子 + 與 - 除了可以作為加法與減法的運算子外, 也可以當成只需要單一運算元的正、負號運算子, 例如:
單運算元的正、負號運算子
單運算元的正、負號運算子 在第 4 行就利用了負號運算子將 1 + 3 的運算結果變成負數。
4-4 布林運算 (Logical Operation) 單運算元的反向運算子 (Complement Operator) 比較運算子 (Comparison Operator) 邏輯運算子 (Logical Operator)
單運算元的反向運算子 (Complement Operator) 反向運算子只需要單一個布林型別的運算元, 其運算結果就是運算元的反向值。也就是說, 如果運算元的值是 true, 那麼反向運算的結果就是 false;反之, 如果運算元的值是 false, 那麼反向運算的結果就是 true。這通常會用來檢查某種狀況是否不成立, 例如:
單運算元的反向運算子 (Complement Operator)
單運算元的反向運算子 (Complement Operator) 在這個例子中, 假設我們使用 isOn 這個布林型別的變數代表房間的燈是否有開, isOn 的值為 true 表示有開、false 表示沒開。
單運算元的反向運算子 (Complement Operator) 那麼在第 4 行中用 if 敘述來判斷 !isOn 的值, 如果 !isOn 的運算結果是 true, 那就表示 isOn 的值是false, 也就是現在沒開燈, 因此顯示現在沒有開燈的訊息。 接著, 利用反向運算將 isOn 的值反向, 然後再使用 if 敘述檢查 isOn 變數的值, 如果是 true, 就表示現在有開燈, 因此列印對應的訊息。
比較運算子 (Comparison Operator) 比較運算子需要兩個數值型別的運算元, 並依據運算子的比較方式, 比較兩個運算元是否滿足指定的關係。下表就是個別運算子所要比較的關係:
比較運算子 (Comparison Operator)
比較運算子 (Comparison Operator) 比較運算子的運算結果是一個布林值, 代表所要比較的關係是否成立。舉例來說:
比較運算子 (Comparison Operator)
比較運算子 (Comparison Operator) 這裡我們將兩個變數的比較關係一一列出, 您可以檢視執行的結果。 == 與!= 運算子除了可以用在數值資料上以外, 也可以用在布林型別的資料, 其餘的比較運算子則只能用在數值資料上。例如:
比較運算子 (Comparison Operator)
邏輯運算子 (Logical Operator) 邏輯運算子就相當於是布林資料的比較運算, 它們都需要兩個布林型別的運算元。各個運算子的意義如下:
邏輯運算子 (Logical Operator) & 與 && 運算子是邏輯且 (AND) 的意思, 當兩個運算元的值都是 true 的時候, 運算結果就是 true, 否則就是 false。 | 與 || 運算子是邏輯或 (OR) 的意思, 兩個運算元中只要有一個是 true, 運算結果就是 true, 只有在兩個運算元的值都是 false 的情況下, 運算結果才會是 false。
邏輯運算子 (Logical Operator) ^ 則是邏輯互斥 (XOR, eXclusive OR) 的運算, 當兩個運算元的值不同時, 運算結果為 true, 否則為 false。 舉例來說:
邏輯運算子 (Logical Operator)
邏輯運算子 (Logical Operator)
邏輯運算子 (Logical Operator) 第 4 、5 行由於 b 是 false, 所以運算結果為 false。第 6 、7 行因為 a 是true, 所以結果是 true。第 8 行因為 a 與 b 的值不同, 所以互斥運算的結果是true。10 ~ 11 行則因為 a 和 c 都是 true, 所以除了互斥運算以外, 其餘的運算結果都是 true。
邏輯運算子 (Logical Operator) 您可能覺得奇怪, &、| 這一組運算子和 &&、|| 這一組運算子的作用好像一模一樣, 為什麼要有兩組功用相同的運算子呢?其實這兩組運算子進行的運算雖然相同, 但是 &&、|| 這一組運算子會在左邊的運算元就可以決定運算結果的情況下忽略右邊運算元。請看以下這個範例:
邏輯運算子 (Logical Operator)
邏輯運算子 (Logical Operator)
邏輯運算子 (Logical Operator) 您可以發現到, 雖然第 7 與 13 行的運算結果都一樣, 但是它們造成的效應卻不同。在第 13 行中, 由於 || 運算子左邊的運算元是 true, 因此不需要看右邊的運算元就可以知道運算結果為 true。 所以 i++ == j 這個運算式根本就不會執行, i 的值也就不會遞增, 最後看到 i 的值原封不動。但反觀第 7 行, 由於是使用 | 運算子, 所以會把兩個運算元的值都求出, 因此就會遞增變數 i 的內容了。
邏輯運算子 (Logical Operator) 依此類推, & 運算子與 && 的運算子也是如此。像這樣只靠左邊的運算元便可推算運算結果, 而忽略右邊運算元的方式, 稱為短路模式 (Short Circuit), 表示其取捷徑, 而不會浪費時間繼續計算右邊運算元的意思。在使用這一類的運算子時, 便必須考量到短路模式的效應, 以避免有些我們以為會執行的動作其實並沒有執行的意外。
4-5 位元運算 (Bitwise Operation) 在 Java 中, 整數型別的資料是以 1 或多個位元組透過 2 進位系統來表示, 例如, 以 Byte 型別的資料來說, 就是用一個 Byte 來表示數值, 其中最高位元是正負號, 像是 2 拆解成 8 個位元就是:
位元運算 (Bitwise Operation) 而負數是以 2 的補數法 (2's Complement), 也就是其絕對值 - 1 的補數 (Complement) 表示, 亦即其絕對值減 1 後以 2 進位表示, 然後將每一個位元的值反向, 例如:
位元邏輯運算子 (Bitwise Logical Operator) 如果運算式的 2 個運算元都是整數, 那麼 ^、| 與 & 進行的並不是前面所介紹的布林值邏輯運算, 而是進行位元邏輯運算, 也就是將 2 個運算元的對應位元兩兩進行邏輯運算, 得到運算式的結果。請看範例:
位元邏輯運算子 (Bitwise Logical Operator)
位元邏輯運算子 (Bitwise Logical Operator) 由於 2 的 2 進位表示法為 00000010, 而 -2 的 2 進位表示法為 11111110, 所以 2 | -2 就是針對對應位元兩兩進行邏輯或的運算, 對應位元中有一個值為 1 則結果即為 1, 否則就是 0 :
位元邏輯運算子 (Bitwise Logical Operator) 結果就是 11111110, 即 -2。2 & -2 就是針對對應位元兩兩進行邏輯且的運算, 只有對應位元的值都是 1 時結果才為 1, 否則即為 0 :
位元邏輯運算子 (Bitwise Logical Operator) 結果就是 00000010, 亦即 2。2 ^ -2 就是針對對應位元兩兩進行邏輯互斥的運算, 當對應位元的值不同時為 1, 否則為 0 : 結果就是 11111100, 亦即 -4。
單運算元的位元補數運算子 (Bitwise Complement Operator) 位元補數運算子只需要一個整數型別的運算元, 運算的結果就是取運算元的 2 進位補數, 也就是將運算元以 2 進位表示後, 每一個位元取反向值, 例如:
單運算元的位元補數運算子 (Bitwise Complement Operator)
單運算元的位元補數運算子 (Bitwise Complement Operator) 由於 2 的 2 進位表示為 00000010, 所以取補數即為將各個位元值反向, 得到 11111101, 為 -3 ;而 -2 的 2 進位表示為 11111110, 取補數即為將各個位元值反向, 得到 00000001, 為 1。
位元移位運算子 (Shift Operator) 位元移位運算子需要 2 個整數型別的運算元, 運算的結果就是將左邊的運算元以 2 進位表示後, 依據指定的方向移動右邊運算元所指定的位數。移動之後空出來的位元則依據運算子的不同會補上不同的值:
位元移位運算子 (Shift Operator) 如果是 >> 運算子, 左邊空出來的所有位元都補上原來最左邊的位元值。 如果是 >> 以外的移位運算子, 那麼空出來的位元都補 0。舉例來說, 如果左邊的運算元是 int 型別的 2, 並且只移動一個位元, 那麼各種移位的運算如下所示:
位元移位運算子 (Shift Operator) 2 >> 1
位元移位運算子 (Shift Operator) 2 >>> 1
位元移位運算子 (Shift Operator) 2 << 1
位元移位運算子 (Shift Operator) 但如果左邊的運算元是 –2, 那麼移動 1 個位元的狀況就會變成這樣: -2 >> 1
位元移位運算子 (Shift Operator) -2 >>> 1
位元移位運算子 (Shift Operator) -2 << 1
移位運算與乘除法 由於移位運算是以位元為單位, 如果是向左移 1 位, 就等於是原本代表 1 的位數移往左成為代表 2 的位數、而原本代表 2 的位數則往左移 1 位變成代表 4 的位數, ..., 依此類推, 最後就相當於把原數乘以 2;如果移 2 位, 就變成乘以 4 了。
移位運算與乘除法 相同的道理, 當使用 >> 時, 右移 1 位的運算就等於是除以 2 、右移 2 位就變成除以 4。對於整數來說, 使用位移運算因為不牽涉到數值的計算, 會比使用除法來的有效率, 可以善加利用。
位元移位運算子 (Shift Operator)
位元移位運算子 (Shift Operator)
型別自動轉換 在使用移位運算時, 請特別注意, 除非左邊的運算元是 long 型別, 否則 Java 會先把左邊運算元的值轉換成 int 型別, 然後才進行移位的運算。 這對於負數的 >>> 運算會有很大的影響。舉例來說, 如果 Shift.java 的第 3 行將 i、j 宣告為 byte, 並且以一個位元組來進行移位運算的話, -2 >>> 1 的結果應該是:
型別自動轉換 不過實際上因為 -2 會先被轉換成 int 型別, 因此移位運算的結果和 Shift.java 一樣, 還是 2147483647。
4-6 運算式的運算順序 雖然已經瞭解了 Java 中大部分運算子的功用, 不過如果不小心, 可能會寫出令您自己意外的程式。舉例來說, 以下這個運算式: 您能夠猜出來變數 i 最後的內容是甚麼嗎?為了確認 i 的內容, 必須先瞭解當一個運算式中有多個運算子時, Java 究竟是如何解譯這個運算式?
運算子間的優先順序 (Operator Precedence) 影響運算式解譯的第一個因素, 就是運算子之間的優先順序, 這個順序決定了運算式中不同種類運算子之間計算的先後次序。請看以下這個運算式:
運算子間的優先順序 (Operator Precedence) 在這個運算式中, 您也許可以依據數學課程中對於四則運算的基本認識, 猜測左邊的加法要比中間的乘法優先順序低, 所以 3 會與乘法運算子結合。 可是中間的乘法和右邊的移位運算哪一個比較優先呢?如果乘法運算子比移位運算子優先, 5 就會選取乘法運算子, 整個運算式就可以解譯成這樣:
運算子間的優先順序 (Operator Precedence) 也就是 那麼接下來的問題就是加法運算子和移位運算子哪一個優先, 以便能夠決定中間的 15 要和加法運算子還是移位運算子結合。以此例來說, 如果加法運算子優先, 也就是 16 >> 1, 變成 8 ;如果是移位運算子優先, 就是 1 + 7, 也是 8。
運算子間的優先順序 (Operator Precedence) 但是如果移位運算子比乘法運算子優先的話, 就會解譯成這樣: 那麼 i 的值就會變成是 1 + 3 * 2, 也就是 1 + 6, 變成 7 了。從這裡就可以看到, 運算子間的優先順序不同, 會導致運算式的運算結果不同。
運算子間的優先順序 (Operator Precedence) Java 制訂了一套運算子之間的優先順序, 來決定運算式的計算順序。以剛剛的範例來說, 乘法運算子最優先, 其次是加法運算子, 最後才是移位運算子, 因此, i 的值實際上會是 8, 就如同第一種解譯的方式一樣。以下是實際的程式:
運算子間的優先順序 (Operator Precedence)
運算子間的優先順序 (Operator Precedence) 從執行結果可以看出來, 第 6 行中間的 2 的確是先選了加法運算子, 否則 i 的值應該是 2;同樣的道理, 第 4 行中, 5 先選了乘法運算子, 否則 i 的值應該是 7。
運算子的結合性 (Associativity) 另外一個影響運算式計算結果的因素, 稱為結合性。所謂的結合性, 是指對於優先順序相同的運算子, 彼此之間的計算順序。請先看以下這個運算式:
運算子的結合性 (Associativity) 由於運算式中都是除法運算子, 優先順序自然相同, 但是左邊的除法先算還是右邊的除法先算, 結果顯然不同。如果以左邊的除法運算子為優先, 就會解譯為這樣: 變數 i 的值就會是 4 / 2, 也就是 2。但如果是以右邊的除法運算子為優先, 就會解譯成這樣:
運算子的結合性 (Associativity) 和運算子間的優先順序一樣, 顯然 Java 必須要有一套規則, 才能在撰寫程式時, 確認運算結果的正確性, 而不會有意外的結果。
運算子的結合性 (Associativity) 以剛剛所舉的除法運算子來說, Java 就規定了它的結合性為左邊優先。也就是說, 當多個除法運算子串在一起時, 會先從左邊的除法運算子開始運算。因此, 在前面的例子中, 會以左邊的除法運算子優先, 計算出的結果再成為第 2 個除法運算子的運算元, 也就是採取第 1 種解譯的方法。來看看實際的程式:
運算子的結合性 (Associativity)
運算子的結合性 (Associativity) 指定運算子的結合律就和除法運算子相反, 是右邊優先, 舉例來說:
運算子的結合性 (Associativity) 其中第 4 行就是依靠指定運算子右邊優先的結合性, 否則如果指定運算子是左邊優先結合的話, 就變成:
運算子的結合性 (Associativity) 如此將無法正常執行, 因為第 2 個指定運算子左邊需要變數作為運算元, 但左邊這個運算式 i = j 的運算結果並不是變數, 而是數值。
以括號強制運算順序 瞭解了運算子的結合性與優先順序之後, 就可以綜合這 2 項特性, 深入瞭解運算式的解譯方法了。底下先列出所有運算子的優先順序與結合性, 方便您判斷運算式的計算過程 (優先等級數目越小越優先):
以括號強制運算順序
以括號強制運算順序
解譯運算式 有了結合性與優先順序的規則, 任何複雜的運算式都可以找出計算的順序, 舉例來說: 要得到正確的計算結果, 先在各個運算子下標示優先等級:
解譯運算式 從優先等級最高的運算子開始, 找出它的運算元, 然後用括號將這個運算子所構成的運算式標示起來, 視為一個整體, 以做為其他運算子的運算元。 如果遇到相鄰的運算元優先等級相同, 就套用結合性, 找出計算順序。依此類推, 一直標示到優先等級最低的運算子為止:
解譯運算式
解譯運算式 所以, 最後變數 i 的值應該是:
解譯運算式 實際程式執行結果如下:
解譯運算式 如果每次看到這樣的運算式, 都要耗費時間才能確定其運算的順序, 不但難以閱讀, 而且撰寫的時候也可能出錯。因此, 建議您最好是使用括號明確的標示出運算式的意圖, 以便讓撰寫程式的您以及可能會閱讀程式的別人都能夠一目了然, 清清楚楚計算的順序。像是剛剛所舉的例子來說, 至少要改寫成這樣, 才不會對於計算的順序有所誤會:
辨識運算子 Java 在解譯一個運算式時, 還有一個重要的特性, 就是會從左往右讀取, 一一辨識出個別的運算子, 舉例來說:
辨識運算子 其中第 4 行指定運算子右邊的運算元如果對於運算子的歸屬解譯不同, 結果就會不同。如果解譯為
辨識運算子 那麼運算結果就是 6, 而且 i 變為 4 、j 的值不變。但如果解譯成這樣:
辨識運算子 事實上, Java 會由左往右, 以最多字元能識別出的運算子為準, 因此真正的結果是第一種解譯方式。 為了避免混淆, 建議您在撰寫這樣的運算式時, 加上適當的括號來明確分隔運算子。
4-7 資料的轉型 (Type Conversion) 到目前為止, 各種運算子的功用以及運算式的運算順序都說明清楚, 不過即便如此, 您還是有可能寫出令自己意外的運算式。 因為 Java 在計算運算式時, 除了套用之前所提到的結合性與優先順序以外, 還使用了幾條處理資料型別的規則, 如果不瞭解這些, 撰寫程式時就會遇到許多奇怪不解的狀況。
數值運算的自動提升 (Promotion) 請先看看以下這個程式:
數值運算的自動提升 (Promotion) 看起來這個程式似乎沒有甚麼問題, 我們把 1 個 byte 型別的變數值右移 1 個位元, 然後再放回變數中, 但是如果您編譯這個程式, 就會看到以下的訊息:
數值運算的自動提升 (Promotion) Java 編譯器居然說可能會漏失資料?其實這並不是您的錯, 而是您不曉得 Java 在計算運算式時所進行的額外動作。以下就針對不同的運算子, 詳細說明 Java 內部的處理方式。
單元運算子 對於單元運算子來說, 如果其運算元的型別是 char、byte、或是 short, 那麼在運算之前就會先將運算元的值提升為 int 型別。
雙運算元的運算子 如果是二元運算子, 規則如下: 如果有任一個運算元是 double 型別, 那麼就將另一個運算元提升為 double 型別。 否則, 如果有任一個運算元是 float, 那麼就將另一個運算元提升為 float 型別。 否則, 就再看是否有任一個運算元是 long 型別, 如果有, 就將另外一個運算元提升為 long 型別。 如果以上規則都不符合, 就將 2 個運算元都提升為 int 型別。
雙運算元的運算子 簡單來說, 就是將兩個運算元的型別提升到同一種型別。根據這樣的規則, 就可以知道剛剛的 Promotion.java 為什麼會有問題了。 由於 Java 會把 >> 運算子兩邊的運算元都提升為 int 型別, 因此 i >> 1 的運算結果也是 int 型別, 但是 i 卻是 byte 型別, 如果想把 int 型別的資料放到 byte 型別的變數中, Java 就會擔心數值過大, 無法符合 byte 型別可以容納的數值範圍。
智慧的整數數值設定 如果是使用字面常數設定型別為 char、byte、或是 short 的變數, 那麼即使 Java 預設會將整數的字面常數視為 int 型別, 但只要該常數的值落於該變數所屬型別可表示的範圍內, 就可以放入該變數中。也就是說, 以下這行程式是可以的:
智慧的整數數值設定 但此項規則並不適用於 long 型別的字面常數, 像是以下這行程式: 編譯時就會錯誤:
智慧的整數數值設定 請務必特別注意。
強制轉型 (Type Casting) 那麼到底要如何解決這個問題呢?如果以保管箱的例子來說, 除非可以動手把物品擠壓成能夠放入保管箱的大小, 否則怎麼樣都放不進去。 在 Java 中也是一樣, 除非您可以自行保證要放進去的數值符合 byte 的可接受範圍, 否則就無法讓您將 int 型別的資料放入 byte 型別的變數中。這個保證的方法就稱為強制轉型 (Cast), 請看以下的程式:
強制轉型 (Type Casting)
強制轉型 (Type Casting) 這個程式和剛剛的 Promoting.java 幾乎一模一樣, 差別只在於將第 4 行中原本的移位運算整個括起來, 並且使用 (byte) 這個轉型運算子 (Casting Operator) 將運算結果轉成 byte 型別。這等於是告訴 Java 說, 我要求把運算結果變成 byte 型別, 後果我自行負責。 透過這樣的方式, 您就可以將運算結果放回 byte 型別的變數中了。
強制轉型的風險 強制轉型雖然好用, 但因為是把數值範圍比較大的資料強制轉換為數值範圍比較小的型別, 因此有可能在轉型後造成資料值不正確, 例如:
強制轉型的風險
強制轉型的風險 在第 6 行中, 因為 i 的值為 3, 符合 byte 型別的範圍, 轉型後並不會有問題。但是第 11 行中, 因為 i 的值為 199, 已經超過 byte 的範圍, 由 int 強制轉型為 byte 時只會留下最低的 8 個位元, 使得轉型後 b 的值變成 11000111, 反而變成 -57 了。
自動轉型 除了運算子所帶來的轉型效應以外, 還有一些規則也會影響到資料的型別, 進而影響到運算式的運算結果, 分別在這一小節中一一探討。
字面常數的型別 除非特別以字尾字元標示, 否則 Java 會將整數的字面常數視為是 int 型別, 而將帶有小數點的字面常數視為是 double 型別, 撰寫程式時, 常常會忽略這一點, 導致得到意外的運算結果, 甚至於無法正確編譯程式。
指定運算子的轉型 在使用指定運算子時, 會依據下列規則將右邊的運算元自動轉型: 如果左邊運算元型別比右邊運算元型別的數值範圍要廣, 就直接將右邊運算元轉型成左邊運算元的型別。
指定運算子的轉型 如果左邊的運算元是 byte、short、或是 char 型別的變數,而右邊的運算元是僅由 byte 、short 、int 、或是 char 型別的字面常數所構成的運算式, 並且運算結果落於左邊變數型別的數值範圍內, 那麼就會將右邊運算元自動轉型為左邊運算元的型別。
指定運算子的轉型 這些規則正是我們之所以能夠撰寫以下這樣程式的原因:
指定運算子的轉型 像是第 3 行指定運算子的右邊就是 int 型別, 但左邊是 byte 型別的變數, 可是因為右邊的運算式僅由字面常數構成, 且計算結果符合 byte 的範圍, 一樣可以放進去。
指定運算子的轉型 第 4 行則很簡單, 右邊 byte 型別的資料可以直接放入左邊 int 型別的變數中。如果您把第 3 行改成這樣:
轉型的種類 在 Java 中, 由 byte 到 int 這種由數值範圍較小的基本型別轉換為範圍較大的基本型別, 稱之為寬化轉型 (Widening Primitive Conversion);反過來的方向, 則稱之為窄化轉型 (Narrowing primitive Conversion)。
4-8 其他運算子 複合指定運算子 (Compound Assignment Operator) 條件運算子 (Conditional Operator)
複合指定運算子 (Compound Assignment Operator) 如果您在進行數值或是位元運算時, 左邊的運算元是個變數, 而且會將運算結果放回這個變數, 那麼可以採用簡潔的方式來撰寫。舉例來說, 以下這行程式: 就可以改寫為
複合指定運算子 (Compound Assignment Operator) 這種作法看起來好像只有節省了一點點打字的時間, 但實際上它還做了其他的事情, 請先看以下的程式:
複合指定運算子 (Compound Assignment Operator) 其中第 6 行的程式如果不使用複合指定運算子, 並不能單純的改成這樣: 因為 Java 會將 4.6 當成 double 型別, 為了讓 i 可以和 4.6 相加, i 的值會先被轉換成 double 型別。因此 i + 4.6 的結果也會是 double 型別, 但 i 卻是 int 型別, 因此指定運算在編譯時會發生錯誤。
複合指定運算子 (Compound Assignment Operator) 正確的寫法應該是: 而這正是複合指定運算子會幫您處理的細節, 它會將左邊運算元的值取出, 和右邊運算元運算之後, 強制將運算結果轉回左邊運算元的型別, 然後再放回左邊的運算元。這樣一來, 您就不需要自己撰寫轉換型別的動作了。
複合指定運算子 (Compound Assignment Operator) 除了 += 以外, *= 、/= 、%= 、-= 、<<= 、>>= 、>>>= 、&= 、^= 、以及 |= 也是可用的複合指定運算子。
條件運算子 (Conditional Operator) 條件運算子是比較特別的運算子, 它總共需要 3 個運算元, 分別以 ? 與 : 隔開。第 1 個運算元必須是一個布林值, 如果這個布林值為 true, 就選取第 2 個運算元進行運算, 否則就選取第 3 個運算元進行運算, 作為整個條件運算的結果。例如:
條件運算子 (Conditional Operator)
條件運算子 (Conditional Operator) 其中, 第 4 行使用了條件運算子來設定變數 i 的值, 3 個運算元分別是 j % 2 == 1 這個比較運算式、2、以及 1。意思就是如果 j % 2 == 1 成立的話, 就將變數 i 內容設為 2, 否則就為 1。由於這裡 j 是 17, 所以設立的條件會成立, 於是變數 i 變成 2 了。
條件運算子的邊際效應 要特別留意的是, 如果第 2 或是第 3 個運算元為一個運算式而且並不是被選取的運算元時, 該運算元並不會進行運算, 例如:
條件運算子的邊際效應 由於第 3 個運算元, 也就是 j++ 並非被選取的運算元, 所以 j++ 並不會執行, 因此變數 j 的內容還是 17。
條件運算子運算結果的型別 使用條件運算子時, 有一個陷阱很容易忽略, 那就是條件運算子運算結果的型別判定。條件運算子依據的規則如下: 如果後 2 個運算元的型別相同, 那麼運算結果的型別也就一樣。 如果第 2 個運算元是 byte 型別, 而第 3 個運算元是 short 型別, 運算結果就是 short 型別。
條件運算子運算結果的型別 如果第 2 個運算元是 byte 、short 、或是 char 型別, 而第 3 個運算元是個由字面常數構成的運算式, 並且運算結果為 int, 而且落於第 2 個運算元型別的數值範圍內, 那麼條件運算子的運算結果就和第 2 個運算元型別相同。 若以上條件均不符合, 則運算結果的型別就依據二元運算子的運算元自動提升規則。
條件運算子運算結果的型別
條件運算子運算結果的型別 第 5 行看起來並沒有甚麼不對, 但是實際上連編譯都無法通過。這是因為依據剛剛的規則, 這一行中的條件運算子會因為第 3 個運算元 i 是 int 型別, 使得運算結果變成 int 型別, 當然就無法放入 byte 型別的 b 中了。此時, 只要把運算結果強制轉型為 byte 型別, 就可以正常執行了。