Java Reference 是什麼?搞懂物件與記憶體背後的運作邏輯【Thinking in Java筆記(1-1)】

Java Reference 是什麼?搞懂物件與記憶體背後的運作邏輯【Thinking in Java筆記(1-1)】
Photo by orbtal media / Unsplash
Java 的物件和參考,到底是什麼鬼?為什麼我明明宣告了變數卻還是出錯?為什麼我用 == 比較字串卻得到 false?這篇文章會用最簡單的語言幫你拆解 Java 背後的記憶體操作與語意邏輯,讓你真正理解「Java 一切皆物件」這句話到底代表什麼

你以為你有物件,其實只有參考

Java 有一句老生常談:「Everything is an Object」。但對新手來說,真正的陷阱藏在那個看似無害的 String s;

String s;

這看起來像是「我創建了一個字串」,其實不是。你只創建了一個「可以指向字串的參考變數」,但它目前什麼都沒指。這就像你手上有一支電視遙控器,但房間裡根本沒電視

如果你硬要拿這支空遙控器按按鈕:

System.out.println(s.length()); // 會直接爆炸

Java 會中斷程式,並丟出錯誤(叫做 NullPointerException),意思是:「你正在對一個不存在的物件做操作,拜託你先搞清楚再來」

你不需要現在就理解什麼是 Exception,只要知道:沒初始化的參考變數不能用,就這麼簡單

✅ 正確的方式是:讓參考指向一個實體

String s = "asdf";

這行程式碼做了兩件事:

  1. JVM 建立了一個字串物件 "asdf",並存到所謂的「字串常量池(String Constant Pool)」中
  2. 變數 s 拿到這個物件的參考

這是一種「隱式的物件建立」,你沒有用 new,但 Java 幫你處理好了

new String("asdf") 是在幹嘛?

你也許會遇到這種寫法:

String s = new String("asdf");

這是「顯式建立物件」的做法。它強迫 JVM 在記憶體的堆區(heap)裡建立一個新的字串,即使 "asdf" 在常量池已經有一份了,它還是會傻傻再給你一份

結果就是這樣:

String a = "asdf";
String b = new String("asdf");

System.out.println(a == b); // false!不是你想的那樣
  • a 指向的是常量池中的字串
  • b 是強制在堆中建立一份新的字串物件
  • 所以他們兩個「長得一樣但住的地方不同」
== 比較的是參考位址,不是內容。
除非你有明確目的,否則不要亂用 new

Java 記憶體分配懶人包

區域內容備註
RegisterCPU 層級快取,碰不到當作不存在就好
Stack存方法區域變數、參考變數用完自動回收,快!
Heapnew 出來的東西住這裡要靠 GC 處理記憶體
常量區字串字面量等不可變資料效能優化用

基本類型 (Primitive Types) vs 包裝器類型 (Wrapper Classes)

Java 有 8 種基本類型(primitive types),是語言中最基礎的資料類型,像是數字、布林值、字元等。它們的特點是:

  • 不是物件
  • 不需要用 new 建立
  • 記憶體中直接儲存值(通常存在 stack 中)
  • 執行效率很高
Primitive大小最小值最大值包裝器類型
boolean---Boolean
char16 bitsUnicode 0Unicode 2<sup>16</sup> - 1Character
byte8 bits-128+127Byte
short16 bits-2<sup>15</sup>+2<sup>15</sup> - 1Short
int32 bits-2<sup>31</sup>+2<sup>31</sup> - 1Integer
long64 bits-2<sup>63</sup>+2<sup>63</sup> - 1Long
float32 bitsIEEE 754IEEE 754Float
double64 bitsIEEE 754IEEE 754Double
void---Void
  • 所有類型都有正負號:Java 中沒有無符號(unsigned)類型
  • boolean 佔用的空間沒有明確指定,僅定義為能夠取字面值 truefalse

Java 提供了這些基本類型對應的「包裝器類型」,也就是物件版本。當你需要把基本類型放入集合、或使用物件語法時,就可以用包裝器

從 Java 5 開始支援 autoboxing/unboxing:

Integer x = 5; // 自動轉成 Integer
int y = x;     // 自動拆箱成 int

BigInteger & BigDecimal:更大更準的數字

Java 提供了兩個高精度計算用的類別,當你覺得 long 還不夠大、double 還不夠準時,就可以召喚這兩個重量級角色:

  • BigInteger:可以儲存無限長的整數(理論上)
  • BigDecimal:可以處理精確的小數,例如金額

這兩個類別沒有對應的 primitive,也比較慢,但非常準

陣列也是物件:Java 中的多個變數怎麼表示?

為什麼需要陣列?

當你需要儲存一群同類型的變數,例如 10 個 int,難道你要寫 int a0, a1, a2... 嗎?這時候,陣列(Array)就是你的朋友

int[] nums = new int[5];

這行程式會:

  1. 在 heap 中建立一個能容納 5 個 int 的陣列物件
  2. 每個值預設是 0(Java 的預設行為)
  3. 回傳參考給變數 nums

你可以透過索引(從 0 開始)操作它:

nums[0] = 10;
System.out.println(nums[0]); // 10

陣列會自動進行範圍檢查,如果你越界:

nums[5] = 42; // 👈 ArrayIndexOutOfBoundsException

這會直接讓程式中斷。這就是 Java 的安全設計:寧可程式炸掉,也不要偷偷寫到奇怪的記憶體位置

Java 的作用域和物件生命週期

在 C/C++ 中,作用域是由大括號的位置決定的。例如:

{
  int x = 12;
  // Only x available
  {
    int q = 96;
    // Both x and q available
  }
  // Only x available
  // q is "out of scope"
}

在 Java 中,變數的作用域由 {} 區塊控制,但你不能在內層重複宣告外層的同名變數:

{
  int x = 12;
  {
    int x = 96; // Illegal: 重複定義
  }
}

這樣設計其實是為了保護你這種會忘記自己在外層寫過什麼的人

參考超出作用域,物件還活著?

Java 中的物件不具備和基本類型一樣的生命週期。當使用 new 建立物件時,該物件可以存活於其作用域之外。例如:

{
  String s = new String("a string");
  // End of scope
}
// reference s 已經消失,但 String 物件仍存在於記憶體中

這就是「Java 幫你管記憶體」的真相。不是你不釋放,而是你根本沒權力釋放。你只是交給 GC 當清潔隊員

總結|你需要記得的關鍵知識點

  • Java 的變數是參考,不是物件本體
  • new 會把東西丟進 Heap,參考變數存在 Stack
  • 基本類型有對應的包裝器類別(自動轉換)
  • 陣列也是物件,有預設初始值與邊界檢查
  • Java 有作用域規則,也有自動記憶體管理(GC)
如果這篇有幫助,記得分享給還在跟 NullPointerException 打架的朋友。你不孤單,大家都曾經拿著沒電的遙控器狂按過