Java 運算子一次搞懂:從基本用法到別名陷阱的完整解析【Thinking in Java筆記(2-1)】

Java 運算子一次搞懂:從基本用法到別名陷阱的完整解析【Thinking in Java筆記(2-1)】
Photo by orbtal media / Unsplash
你知道 a = b 在物件上不是「複製」而是「結拜」嗎?
你以為 + 只是加法?其實它也能把字串和整數亂湊一通

基礎觀念:什麼是運算子?

Java 中的運算子是用來接收一個或多個值(也稱為操作數,Operands)然後算出結果的符號。像是我們熟悉的 +-*/= 都是常見的運算子

不過有些運算子會偷偷改變變數本身的值,像是這些:

  • ++(自增)
  • --(自減)
  • =(賦值)

來個例子讓你印象深刻:

int x = 5;
x++;  // x 現在是 6

運算子 vs 方法

你可以用 x + y,也可以用 add(x, y)。功能一樣,但寫法不同。運算子比較短,方法比較明白

int sum = x + y;     // 運算子寫法
int sum = add(x, y); // 方法寫法

兩種寫法沒有差別,只是風格不同而已

常見 Java 運算子分類快速表

類型 例子 說明
算術運算子 + - * / % 基本數學運算
賦值運算子 = 將值給變數
比較運算子 == != > < >= <= 判斷條件
邏輯運算子 `&&
位元運算子 `& ^ ~ << >>`
字串運算子 + += 字串拼接超好用
在 Java 裡,只有 + 可以拼字串,其他都是給數字玩的
String message = "Hello, " + 42; // 會變成 "Hello, 42"
提示:+ 運算子用於字串和非字串類型的組合時,Java 編譯器會自動將非字串元素轉換為 String。這使得連接 String 和其他數據類型變得非常方便

運算子優先順序

Java 跟數學一樣有「先乘除後加減」的規則。如果你不加括號,它就照自己的邏輯來算

public class Precedence {
  public static void main(String[] args) {
    int x = 1, y = 2, z = 3;
    int a = x + y - 2/2 + z; // 計算順序:2/2 -> x + y -> 結果 - 1 -> 加上 z
    int b = x + (y - 2)/(2 + z); // 括號內優先:y - 2 和 2 + z,然後計算除法,最後加上 x
    System.out.println("a = " + a + " b = " + b);
  }
}
/* 輸出:
a = 5 b = 1
*/

賦值(Assignment)與物件引用的陷阱:你以為在複製,其實只是搬過去一起住

什麼是「左值」和「右值」?

在 Java 中,賦值語句長這樣:

a = b

這裡的 a 稱為「左值」(L-value),它代表的是一個可以儲存資料的變數位置。而 b 是「右值」(R-value),它是一個可以產生資料內容的東西,可以是變數、常數或表達式

int a;
a = 4; // 合法
4 = a; // 不合法,因為 4 不是變量,無法存儲值

基本類型資料的賦值行為:純複製,好懂又安全

Java 的基本類型(primitive types)像是 int、double、char 等,是直接複製「值」,彼此互不干涉:

int a = 10;
int b = a;
b = 20;
System.out.println(a); // a 仍然是 10

這種行為很單純,左值拿到的是右值的內容副本

物件的賦值行為:別名現象(Aliasing)

物件類型的賦值不會複製內容,而是複製引用(Reference)

class Tank {
  int level;
}

public class Assignment {
  public static void main(String[] args) {
    Tank t1 = new Tank();
    Tank t2 = new Tank();
    t1.level = 9;
    t2.level = 47;
    System.out.println("1: t1.level: " + t1.level + ", t2.level: " + t2.level);
    t1 = t2;
    System.out.println("2: t1.level: " + t1.level + ", t2.level: " + t2.level);
    t1.level = 27;
    System.out.println("3: t1.level: " + t1.level + ", t2.level: " + t2.level);
  }
}
/* 輸出:
1: t1.level: 9, t2.level: 47
2: t1.level: 47, t2.level: 47
3: t1.level: 27, t2.level: 27
*/
  • t1 = t2t2 的參考位置給了 t1,兩個變數現在都指向同一個物件
  • 改 t1.level 就像改 t2.level,因為根本就是同一間房子
如果你想真的「複製」一個物件,要自己 new 一個新物件,或用 clone、copy constructor

方法傳參也是引用複製:參數看起來像影分身,實際上是一條繩子綁在同一個物件上

class Letter {
  char c;
}

public class PassObject {
  static void f(Letter y) {
    y.c = 'z';
  }
  public static void main(String[] args) {
    Letter x = new Letter();
    x.c = 'a';
    System.out.println("1: x.c: " + x.c);
    f(x);
    System.out.println("2: x.c: " + x.c);
  }
}
/* 輸出:
1: x.c: a
2: x.c: z
*/
  • 雖然 lx 的一個副本,但它們都指向同一個 Letter 物件
  • 所以在 change 方法中改 l.c,實際上也是在改 x.c

總結 alias 怎麼辦?

  1. 想要複製內容,就自己 new 一個物件來放。
  2. 別把「複製參考」當作「複製物件」。
  3. 不確定是不是 alias 時,就不要改物件屬性。

寫程式的時候,最怕不是 bug,而是你根本不知道你在改哪一個記憶體位置

總結

本文深入探討了 Java 中運算子的基本概念、優先級以及賦值行為。你應該已經了解了:

  • 運算子如何與操作數交互並生成新值
  • 優先順序怎麼影響你的表達式結果
  • 基本類型 vs 物件類型的賦值差異
  • 別名現象與傳參時的物件共享問題

理解這些概念對於編寫高效且無錯的 Java 程式至關重要。你不需要死背,但至少別再對著 t1 = t2 感到疑惑