Thinking in Java(1-1) 深入理解 Java 中的物件與參考

Thinking in Java(1-1) 深入理解 Java 中的物件與參考
Photo by orbtal media / Unsplash

在 Java 的世界裡,一切都是物件(Object)。理解物件與參考(Reference)的運作方式,是掌握 Java 程式設計的基石。本文將帶你深入探討 Java 中的物件操作、記憶體分配、基本類型(Primitive Types)、包裝器(Wrapper Classes)、陣列(Arrays)以及作用域(Scope)的相關概念。

使用 Reference 操作 Object

一切皆物件 (Everything is an Object)

在 Java 中,所有事物都被視為物件。然而,聲明一個變數時,實際上建立的只是對物件的參考(reference),而非物件本身。例如:

String s;

這行程式碼僅僅是建立了一個 String 類型的參考 s,但尚未指向任何實際的 String 物件。如果此時向 s 發送消息(例如調用方法),將會導致執行時錯誤(Runtime Error)。

安全的初始化方式

為了避免這類錯誤,建議在建立參考的同時進行初始化:

String s = "asdf";

這樣,s 不僅是一個參考,還指向了一個實際的 String 物件。

必須由你建立所有 Object

一旦建立了參考,你通常希望它與某個物件相關聯。這通常是通過使用 new 關鍵字來實現的。new 的意思是「給我一個新的物件」。例如:

String s = new String("asdf");

這行程式碼明確地在堆(Heap)中建立了一個新的 String 物件,並將其參考賦值給 s

物件的儲存位置

Java 程式在運行時,會將物件存放在記憶體的不同區域。了解這些區域有助於編寫高效的程式碼。

寄存器(Register)

寄存器是最靠近 CPU 的記憶體儲存區,存取速度最快。然而,寄存器的數量極為有限,且無法直接控制。C/C++ 語言允許開發者向編譯器建議寄存器的分配方式,但在 Java 中,這並不適用。

堆疊(Stack)

堆疊位於通用 RAM 中,透過堆疊指標(Stack Pointer)的上下移動來分配和釋放內存。堆疊的存取速度僅次於寄存器,且具有高效的記憶體管理方式。然而,Java 系統必須知道堆疊中所有項目的確切生命週期,這限制了程式的靈活性。堆疊中通常存放參考,但實際的物件並不會放在這裡。

堆(Heap)

堆也是位於 RAM 的通用內存池,用來存放所有 Java 物件。相比堆疊,堆具有更大的彈性,因為編譯器不需要知道數據的存活時間。使用 new 關鍵字可以自動在堆中分配內存。然而,這種彈性也帶來了代價:堆的分配和清理比堆疊更耗時。

常量儲存

常量通常存放在程式碼區塊(Code Segment)內部。由於程式碼區塊不會被修改,這種做法保護了常量的不變性。

非 RAM 儲存

非 RAM 儲存指的是在進程控制之外的資料,例如流(Stream)、檔案(File)等。

物件中的特例:基本類型(Primitive Types)

在程式設計中,經常使用一系列小而簡單的類型,我們將它們稱為基本類型(Primitive Types)。這些類型不需要使用 new 來建立物件,因為這樣做效率不高。Java 與 C++ 採用相同的方法,直接在堆疊中建立 非參考 的自動變數,直接儲存值,因而更加高效。

基本類型一覽

Java 的每種基本類型在不同硬體架構下的大小都是固定的。以下是 Java 中所有基本類型的詳細資訊:

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

包裝器(Wrapper Classes)

每種基本類型都有對應的包裝器類型,用於在堆中建立一個非基本類型的物件。這對於需要物件形式的情境(如集合框架)非常有用。

char c = 'x';
Character ch = new Character(c);
Character ch = new Character('x');

// Java SE5 自動包裝功能會自動將 primitive 轉換為包裝器類型
Character ch = 'x';
// 反向轉換
char c = ch;

Java SE5 引入了自動包裝(Autoboxing)和自動拆箱(Unboxing),使得基本類型和包裝器類型之間的轉換更加便捷。

高精度數字

Java 提供了兩個高精度計算用的類別:

  • BigInteger:支援任意長度的整數而不會丟失資料。
  • BigDecimal:支援任意長度的浮點數,可以進行精確的貨幣計算。

這兩個類別雖然在功能上類似於包裝器類型,但沒有對應的基本類型。它們的方法需要通過函數調用來使用,這使得運算速度相對較慢,但提供了更高的精度。

Java 中的陣列(Array)

在 Java 中,陣列是一種特殊的物件,具有以下特性:

  • 初始化保證:Java 會確保陣列在創建時被初始化。
  • 範圍檢查:不能在範圍之外訪問陣列元素,否則會拋出 ArrayIndexOutOfBoundsException
  • 引用的創建:當創建一個陣列時,實際上是創建了一個指向陣列的參考。

例如:

int[] numbers = new int[5];

這行程式碼創建了一個包含 5 個整數的陣列,並將其參考賦值給 numbers

Java 的作用域(Scope)

Java 與 C/C++ 的作用域差異

在 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 物件仍存在於記憶體中

當參考 s 超出作用域後,s 指向的 String 物件仍會保留在記憶體中,直到垃圾回收器(Garbage Collector,GC)將其回收。Java 的垃圾回收機制能自動監視並回收不再被引用的物件,避免了程式人員需要手動管理記憶體,從而消除了因忘記釋放記憶體而導致的內存洩漏問題。

總結

理解 Java 中物件與參考的運作方式、記憶體的分配區域、基本類型與包裝器類型的區別、以及作用域的管理,對於編寫高效、穩定的 Java 程式至關重要。通過掌握這些核心概念,你將能更自信地在 Java 的世界中駕馭各種挑戰。

希望這篇文章能夠幫助你更深入地理解 Java 的基礎概念,並在你的程式設計旅程中提供有價值的參考。