Java 邏輯與位元運算子完全教學:搞懂 &&、|、^、~、位移 >>> 的所有坑點與實例【Thinking in Java 筆記 2-3】

Java 邏輯與位元運算子完全教學:搞懂 &&、|、^、~、位移 >>> 的所有坑點與實例【Thinking in Java 筆記 2-3】
Photo by orbtal media / Unsplash

想寫更高效的 Java 程式碼,卻常在 &&|| 之間猶豫、對 >>> 一臉問號?
這篇文章一次帶你了解 Java 中的邏輯運算子直接常數位元運算子位移運算子,讓你從「這行是什麼意思」的困惑中畢業

不論你是正在讀《Thinking in Java》的學生,還是為了 Leetcode 題目查 >>>>> 差別的工程師,都能在這篇找到答案

Java 邏輯運算子:&&、||、! 的正確用法與短路邏輯

Java 中常用的邏輯運算子有:

  • &&(AND)
  • ||(OR)
  • !(NOT)

這些運算子只能作用在 boolean 表達式上。與 C/C++ 不同,Java 不允許非 boolean 值直接用於邏輯條件中

// C++ 這樣是合法的
while (node) {}

// Java 必須寫成
while (node != null) {}

短路運算(Short-circuiting)

短路邏輯是 Java 中邏輯運算的基本策略,意思是當邏輯運算子左側的條件已足以判定整體結果時,右側的運算就不再執行

  • &&(AND):如果左邊是 false,整體結果一定是 false,所以右邊不會執行這
  • ||(OR):如果左邊是 true,整體結果已確定為 true,右邊也不會執行
有助於提高效能,也可避免不必要的副作用或錯誤(例如 &&右邊包含可能觸發例外的程式碼)

這種「只做必要運算」的特性稱為短路求值(short-circuit evaluation),不只是語法設計,更是程式設計時常用來保護邏輯判斷與效能優化的重要技巧

public class ShortCircuit {
  static boolean test1(int val) {
    print("test1(" + val + ")");
    print("result: " + (val < 1));
    return val < 1;
  }
  static boolean test2(int val) {
    print("test2(" + val + ")");
    print("result: " + (val < 2));
    return val < 2;
  }
  static boolean test3(int val) {
    print("test3(" + val + ")");
    print("result: " + (val < 3));
    return val < 3;
  }
  public static void main(String[] args) {
    boolean b = test1(0) && test2(2) && test3(2);
    print("expression is " + b);
  }
}
/* Output:
test1(0)
result: true
test2(2)
result: false
expression is false
*/

在上述程式中,test2(2) 返回 false,使得整個表達式的結果為 false,因此 test3(2) 不會被執行


Java 中的直接常數(Literals):十六進位、八進位與浮點數尾碼

直接常數是指在程式碼中直接寫出來的數值,例如 420xFF3.14 這些數值字面量。Java 編譯器通常可以根據你寫的位置推斷這些數值是什麼型別,但如果你寫得太曖昧,它會拒絕幫你猜

為了讓程式更清楚明確,也為了避免型別不符錯誤,我們會在某些常數後面加上尾碼(suffix),像是:

  • 1.0f:加上 f 表示這是 float,否則預設是 double
  • 123L:加上 L 表示這是 long,否則預設是 int

這樣做可以讓數值常數與變數型別正確對應,避免不必要的型別轉換錯誤,也提升程式可讀性與安全性

Java 支援多種寫法的直接常數:

int i1 = 0x2f;  // 十六進位
int i2 = 0177;  // 八進位
char c = 0xffff; // 最大 char 值
float f = 1.0f;  // float 必須加 f
long l = 123L;   // long 建議加 L
public class Literals {
  public static void main(String[] args) {
    int i1 = 0x2f; // Hexadecimal (lowercase)
    print("i1: " + Integer.toBinaryString(i1));
    int i2 = 0X2F; // Hexadecimal (uppercase)
    print("i2: " + Integer.toBinaryString(i2));
    int i3 = 0177; // Octal (leading zero)
    print("i3: " + Integer.toBinaryString(i3));
    char c = 0xffff; // max char hex value
    print("c: " + Integer.toBinaryString(c));
    byte b = 0x7f; // max byte hex value
    print("b: " + Integer.toBinaryString(b));
    short s = 0x7fff; // max short hex value
    print("s: " + Integer.toBinaryString(s));
    long n1 = 200L; // long suffix
    long n2 = 200l; // long suffix (but can be confusing)
    long n3 = 200;
    float f1 = 1;
    float f2 = 1F; // float suffix
    float f3 = 1f; // float suffix
    double d1 = 1d; // double suffix
    double d2 = 1D; // double suffix
    // (Hex and Octal also work with long)
  }
} /* Output:
i1: 101111
i2: 101111
i3: 1111111
c: 1111111111111111
b: 1111111
s: 111111111111111
*/
超出型別範圍的值會導致編譯錯誤
若編譯器能夠自動識別型別,可以省略型別後綴

指數表示法(Exponential Notation)

指數表示法(Exponential Notation)是一種簡潔表示非常大或非常小數字的方式,特別常見於浮點數的表達。例如:

float expFloat = 1.39e-43f;
double expDouble = 47e47;

這裡的 eE 表示「乘以 10 的某次方」,所以 1.39e-43 就等於 $$1.39 \times 10^{-43}$$

Java 中使用指數表示法的注意事項:
  • 大小寫皆可eE 意義相同,單純視覺差異
  • 型別尾碼很重要:若你寫 1.39e-43 而沒加 f,Java 會把它當作 double;若你指定給 float 變數,會發生編譯錯誤
  • 精度限制float 精度有限,使用極小數字(像 1.39e-43f)時可能出現誤差或非預期結果
public class Exponents {
  public static void main(String[] args) {
    // Uppercase and lowercase 'e' are the same:
    float expFloat = 1.39e-43f;
    expFloat = 1.39E-43f;
    System.out.println(expFloat);
    double expDouble = 47e47d; // 'd' is optional
    double expDouble2 = 47e47; // Automatically double
    System.out.println(expDouble);
  }
} /* Output:
1.39E-43
4.7E48
*/

如果你是初學者,簡單記得:浮點數常數要存進 float 變數,就加 f;要存進 double,就可以省略

float expFloat = 1.39e-43f;
double expDouble = 47e47;

位元運算子(Bitwise Operators)整理包:^、|、&、~ 的直覺對照與實例

Java 中的位元運算子主要用於對整數型別(如 intlong)的二進位位元進行操作:

運算子說明範例
&位元 AND12 & 6 = 4
|位元 OR12 | 6 = 14 
^位元 XOR12 ^ 6 = 10
~位元反轉~12 = -13

範例:

// XOR: ^
// AND: &
// OR:  |
// NOT: ~

// 定義變數
int a = 12;  // 二進制表達為 1100
int b = 6;   // 二進制表達為 0110

// XOR: ^ (異或)
int resultXor = a ^ b;  // 結果是 10 (二進制為 1010)

// AND: & (與)
int resultAnd = a & b;  // 結果是 4 (二進制為 0100)

// OR: | (或)
int resultOr = a | b;  // 結果是 14 (二進制為 1110)

// NOT: ~ (非)
int resultNot = ~a;  // -13,對 a 的每一位進行反轉

複合指定運算子

這些位元運算子也可以與 = 結合成指定運算子,讓程式更簡潔

a ^= b;  // a 現在等於 10
a |= b;  // a 現在等於 14
a &= b;  // a 現在等於 4
注意:~一元運算子,不能與 = 結合,不能寫成 a ~= b,那是違法語法

位移運算子(Shift Operators)完全圖解:<<、>>、>>> 差異 + 陷阱

位移運算子是 Java 中針對整數型別(如 int、long)進行位元操作的一種工具。簡單來說,它們能夠「把一串 0 和 1 數值往左或往右推」,就像搬磚頭一樣,只是你在搬的是數位的磚

常見的三種位移運算子

運算子中文說法行為說明
<<左移將位元往左推,右邊(低位)補 0,相當於數值 * 2 的 n 次方
>>有號右移將位元往右推,保留符號位(最高位),負數補 1,正數補 0
>>>無號右移將位元往右推,高位一律補 0,忽略正負號,常用於位元資料處理

它們到底在幹嘛?從實例來看

int x = 4;        // 二進位: 00000000 00000000 00000000 00000100
int left = x << 1;  // 左移一位:變成 00000000 00000000 00000000 00001000 -> 8

int y = -8;       // 二進位: 11111111 11111111 11111111 11111000
int signed = y >> 1;  // 有號右移:最高位補 1 -> 負數仍為負數
int unsigned = y >>> 1; // 無號右移:最高位補 0 -> 整數變大,可能變成很大的正數

那這東西有什麼用?

  • << 左移一位 = 原數值 * 2;左移兩位 = * 4;以此類推。這對於做快速乘法位元遮罩運算很常見(尤其在嵌入式、圖像處理、hash 算法等領域)
  • >> 常用於將一個數值縮小(相當於除以 2),而且會保留符號,適合用在邏輯運算或補償負數的情況
  • >>> 被稱為「無符號右移」,在處理二進位資料(例如加密、壓縮、Hash)時很重要,因為它完全忽略符號,能確保資料移動時不會因為負數而亂掉

常見踩雷區:byte 和 short 的自動升級

在 Java 中,小型整數像是 byteshort 在做位移時會自動升級為 int,這導致你可能會看到超奇怪的結果

byte b = -1;
System.out.println(Integer.toBinaryString(b)); // 11111111
System.out.println(Integer.toBinaryString(b >>> 2));
// 11111111111111111111111111111111

這是因為 Java 會先把 b 變成 int(補滿 32 位元)再做位移,導致你以為的 8 位元其實已經擴充變形了。要避免這種錯誤,你應該手動轉型或改用 int

複合指定運算子(讓你寫得更短)

位移運算子也支援複合指定方式:

int a = 8;
a <<= 1; // 相當於 a = a << 1;結果是 16

支援的有:

  • <<=
  • >>=
  • >>>=

範例:


public class URShift {
  public static void main(String[] args) {
    int i = -1;
    print(Integer.toBinaryString(i));
    i >>>= 10;
    print(Integer.toBinaryString(i));
    long l = -1;
    print(Long.toBinaryString(l));
    l >>>= 10;
    print(Long.toBinaryString(l));
    short s = -1;
    print(Integer.toBinaryString(s));
    s >>>= 10;
    print(Integer.toBinaryString(s));
    byte b = -1;
    print(Integer.toBinaryString(b));
    b >>>= 10;
    print(Integer.toBinaryString(b));
    b = -1;
    print(Integer.toBinaryString(b));
    print(Integer.toBinaryString(b>>>10));
  }
} /* Output:
11111111111111111111111111111111
1111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
1111111111111111111111
*/
  • 對於 int 型別,-1 的二進位表示為全 1
  • 使用無符號右移 >>> 後,高位補 0,結果位元數減少
  • 對於 shortbyte,在位移操作時被提升為 int,可能導致預期外的結果

結論

學會這些邏輯與位元運算子,並不只是為了寫出更簡潔的程式碼,它們是理解 Java 在底層如何運作、如何有效控制流程與效能的核心概念。從 && 的短路判斷,到 ^ 的異或操作,再到 >>> 的無號右移,每個看似冷門的符號背後都有實際應用場景

如果你曾經被 compiler 抱怨型別錯誤、在 debug 時不懂為什麼變數值突然爆掉,那麼這篇應該幫你補上了那些「本來只在考卷上出現」的概念
希望這份整理能成為你未來寫程式時的查表夥伴,或至少在你 Google java >>> 差別 時,能少花點時間看 StackOverflow 吵架