Java Reference 是什麼?搞懂物件與記憶體背後的運作邏輯【Thinking in Java筆記(1-1)】
Java 的物件和參考,到底是什麼鬼?為什麼我明明宣告了變數卻還是出錯?為什麼我用 ==
比較字串卻得到 false?這篇文章會用最簡單的語言幫你拆解 Java 背後的記憶體操作與語意邏輯,讓你真正理解「Java 一切皆物件」這句話到底代表什麼
你以為你有物件,其實只有參考
Java 有一句老生常談:「Everything is an Object」。但對新手來說,真正的陷阱藏在那個看似無害的 String s;
String s;
這看起來像是「我創建了一個字串」,其實不是。你只創建了一個「可以指向字串的參考變數」,但它目前什麼都沒指。這就像你手上有一支電視遙控器,但房間裡根本沒電視
如果你硬要拿這支空遙控器按按鈕:
System.out.println(s.length()); // 會直接爆炸
Java 會中斷程式,並丟出錯誤(叫做 NullPointerException
),意思是:「你正在對一個不存在的物件做操作,拜託你先搞清楚再來」
你不需要現在就理解什麼是 Exception,只要知道:沒初始化的參考變數不能用,就這麼簡單
✅ 正確的方式是:讓參考指向一個實體
String s = "asdf";
這行程式碼做了兩件事:
- JVM 建立了一個字串物件
"asdf"
,並存到所謂的「字串常量池(String Constant Pool)」中 - 變數
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 記憶體分配懶人包
區域 | 內容 | 備註 |
---|---|---|
Register | CPU 層級快取,碰不到 | 當作不存在就好 |
Stack | 存方法區域變數、參考變數 | 用完自動回收,快! |
Heap | new 出來的東西住這裡 | 要靠 GC 處理記憶體 |
常量區 | 字串字面量等不可變資料 | 效能優化用 |
基本類型 (Primitive Types) vs 包裝器類型 (Wrapper Classes)
Java 有 8 種基本類型(primitive types),是語言中最基礎的資料類型,像是數字、布林值、字元等。它們的特點是:
- 不是物件
- 不需要用
new
建立 - 記憶體中直接儲存值(通常存在 stack 中)
- 執行效率很高
Primitive | 大小 | 最小值 | 最大值 | 包裝器類型 |
---|---|---|---|---|
boolean | - | - | - | Boolean |
char | 16 bits | Unicode 0 | Unicode 2<sup>16</sup> - 1 | Character |
byte | 8 bits | -128 | +127 | Byte |
short | 16 bits | -2<sup>15</sup> | +2<sup>15</sup> - 1 | Short |
int | 32 bits | -2<sup>31</sup> | +2<sup>31</sup> - 1 | Integer |
long | 64 bits | -2<sup>63</sup> | +2<sup>63</sup> - 1 | Long |
float | 32 bits | IEEE 754 | IEEE 754 | Float |
double | 64 bits | IEEE 754 | IEEE 754 | Double |
void | - | - | - | Void |
- 所有類型都有正負號:Java 中沒有無符號(unsigned)類型
- boolean 佔用的空間沒有明確指定,僅定義為能夠取字面值
true
或false
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];
這行程式會:
- 在 heap 中建立一個能容納 5 個 int 的陣列物件
- 每個值預設是 0(Java 的預設行為)
- 回傳參考給變數
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
打架的朋友。你不孤單,大家都曾經拿著沒電的遙控器狂按過
Comments ()