跳轉到內容

物件生命週期

75% developed
來自華夏公益教科書

導航 類和物件 主題:v  d  e )

在建立 Java 物件之前,必須從檔案系統(副檔名為 .class)載入類位元組碼到記憶體中。這個過程稱為 _類載入_,它包括查詢給定類名的位元組碼,並將該程式碼轉換為 Java 例項。對於每種型別的 Java 類,都會建立一個類。

Java 程式中的所有物件都在堆記憶體中建立。物件是根據其類建立的。可以將類視為藍圖、模板或建立物件的描述。當建立物件時,會分配記憶體來儲存物件的屬性。還會建立一個指向該記憶體位置的物件引用。為了在將來使用該物件,必須將該物件引用儲存為區域性變數或物件成員變數。

Java 虛擬機器 (JVM) 會跟蹤物件引用的使用情況。如果不再有指向該物件的引用,該物件就無法再使用,並將成為垃圾。一段時間後,堆記憶體將充滿未使用的物件。JVM 會收集這些垃圾物件,並釋放它們分配的記憶體,以便在建立新物件時可以再次使用該記憶體。請參見以下簡單示例

Example 程式碼部分 4.30:物件建立。
{
  // Create an object
  MyObject obj = new MyObject();

  // Use the object
  obj.printMyValues();
}

obj 變數包含指向從 MyObject 類建立的物件的物件引用。obj 物件引用在 { } 內處於 作用域 中。在 } 之後,該物件將成為垃圾。物件引用可以傳遞給方法,也可以從方法中返回。

使用 new 關鍵字建立物件

[edit | edit source]

99% 的新物件都是使用 new 關鍵字建立的。

Computer code 程式碼清單 4.13:MyProgram.java
public class MyProgram {

  public static void main(String[] args) {
    // Create 'MyObject' for the first time the application is started
    MyObject obj = new MyObject();
  }
}

當第一次從 MyObject 類建立物件時,JVM 會搜尋檔案系統以查詢類的定義,即 Java 位元組碼。該副檔名為 *.classCLASSPATH 環境變數包含儲存 Java 類的位置。JVM 會查詢 MyObject.class 檔案。根據該類所屬的包,包名將轉換為目錄路徑。

找到 MyObject.class 檔案後,JVM 的類載入器會將該類載入到記憶體中,並建立一個 java.lang.Class 物件。JVM 會將程式碼儲存在記憶體中,為 static 變數分配記憶體,並執行任何靜態初始化塊。此時不會為物件成員變數分配記憶體,將在建立類例項(物件)時為它們分配記憶體。

可以建立任意多個來自同一類的物件。無論建立了多少個物件,程式碼和 static 變數都只儲存一次。將在建立物件時為物件成員變數分配記憶體。因此,物件的尺寸不是由其程式碼的尺寸決定,而是由它儲存成員變數所需的記憶體決定。

透過克隆物件建立物件

[edit | edit source]

類不自動支援克隆。不過,由於所有 Java 物件都繼承了 protected Object clone() 方法,因此可以獲得一些幫助。此基本方法會分配記憶體,並逐位複製物件的狀態。

您可能會問為什麼要使用這種克隆方法。難道不能建立一個建構函式,傳入同一個物件,然後逐個變數進行復制嗎?例如(請注意,訪問 obj 的私有 memberVar 變數是合法的,因為這是在同一個類中)

Computer code 程式碼清單 4.14:MyObject.java
public class MyObject {
   private int memberVar;
...
   MyObject(MyObject obj) {
      this.memberVar = obj.memberVar;
    ...
   }
...
}

這種方法有效,但使用new關鍵字建立物件很耗時。clone()方法在一個操作中複製整個物件的記憶體,這比使用new關鍵字並複製每個變數快得多,因此如果您需要建立大量相同型別的物件,建立單個物件並從其克隆新物件會提高效能。請參見下面使用克隆返回新物件的工廠方法。

Example 程式碼部分 4.31:物件克隆。
HashTable cacheTemplate = new HashTable();
...
/** Clone Customer object for performance reason */
public Customer createCustomerObject() {
   // See if a template object exists in our cache
   Customer template = cacheTemplate.get("Customer");
   if (template == null) {
      // Create template
      template = new Customer();
      cacheTemplate.put("Customer", template);
   }
   return template.clone();
}

現在,讓我們看看如何使Customer物件可克隆。

  1. 首先,Customer類需要實現Cloneable介面。
  2. 覆蓋並使clone()方法public,因為在Object類中它是protected
  3. 在您的clone方法的開頭呼叫super.clone()方法。
  4. 覆蓋Customer的所有子類的clone()方法。
Computer code 程式碼清單 4.15:Customer.java
public class Customer implements Cloneable {
 ...
    public Object clone() throws CloneNotSupportedException {
       Object obj = super.clone();

       return obj;
    }
}

程式碼清單 4.15中,我們使用克隆來加快物件建立速度。克隆的另一個用途是獲取可能隨時間變化的物件的快照。假設我們想將Customer物件儲存在集合中,但我們想將它們與“即時”物件分離。因此,在新增物件之前,我們克隆它們,這樣如果原始物件從那時起發生變化,新增的物件就不會受到影響。另外,假設Customer物件引用一個包含客戶活動的Activity物件。現在我們遇到了一個問題,克隆Customer物件是不夠的,我們還需要克隆引用的物件。解決方法

  1. 使Activity類也可克隆
  2. 確保如果Activity類具有其他“可變”物件引用,則也必須克隆它們,如下所示
  3. 按如下方式更改Customer類的clone()方法
Computer code 程式碼清單 4.16:Customer.java
public class Customer implements Cloneable {
  Activity activity;
  ...
    public Customer clone() throws CloneNotSupportedException {
      Customer clonedCustomer = (Customer) super.clone();

      // Clone the object referenced objects
      if (activity != null) {
         clonedCustomer.setActivity((Activity) activity.clone());
      }
      return clonedCustomer;
   }
}

請注意,只有可變物件需要克隆。克隆物件可以使用對不可變物件(如String)的引用,而無需擔心。

重新建立從遠端來源接收的物件

[edit | edit source]

當物件透過網路傳送時,需要在接收主機上重新建立該物件。

物件序列化
術語物件序列化是指將物件轉換為位元組流的行為。位元組流可以儲存在檔案系統中或透過網路傳送。
稍後,可以從該位元組流重新建立物件。唯一的要求是,在序列化物件和重新建立物件時,必須始終使用相同的類。如果發生在不同的伺服器上,則兩個伺服器上必須存在相同的類。相同的類意味著必須存在完全相同的類版本,否則將無法重新建立物件。對於使用java序列化來使物件持久化或透過網路傳送物件的應用程式來說,這是一個維護問題。
當修改類時,可能會出現使用先前版本的類序列化時重新建立這些物件的問題。

Java使用Serializable介面內建支援序列化;但是,類必須首先實現Serializable介面。

預設情況下,類在轉換為資料流時會將其所有欄位序列化(跳過transient欄位)。如果除了寫入所有欄位的預設操作外,還需要其他處理,則需要為以下三種方法提供實現

private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

如果物件需要在序列化期間寫入或提供替換物件,則需要實現以下兩種方法,並使用任何訪問說明符

Object writeReplace() throws ObjectStreamException;
Object readResolve() throws ObjectStreamException;

通常,對類的微小更改會導致序列化失敗。您仍然可以透過定義序列化版本 ID 來允許載入該類

Example 程式碼部分 4.32:序列化版本 ID。
private static final long serialVersionUID = 42L;

銷燬物件

[edit | edit source]

與許多其他面向物件的程式語言不同,Java執行自動垃圾收集 - 所有未引用的物件都會自動從記憶體中刪除 - 並禁止使用者手動銷燬物件。

finalize()

[edit | edit source]

當物件被垃圾收集時,程式設計師可能希望手動執行清理,例如關閉所有開啟的輸入/輸出流。要實現這一點,可以使用finalize()方法。請注意,finalize()不應該手動呼叫,除了從派生類的finalize方法呼叫超類的finalize方法。此外,我們無法確定何時呼叫finalize()方法。如果Java應用程式在物件被垃圾收集之前退出,則可能永遠不會呼叫finalize()方法。

Example 程式碼部分 4.33:終結。
protected void finalize() throws Throwable {
  try {
    doCleanup();        // Perform some cleanup.  If it fails for some reason, it is ignored.
  } finally {
    super.finalize(); // Call finalize on the parent object
  }
}

垃圾收集器執行緒的優先順序低於其他執行緒。如果應用程式建立物件的速率快於垃圾收集器回收記憶體的速率,程式可能會耗盡記憶體。

僅當存在超出Java虛擬機器直接控制的資源需要清理時,才需要finalize方法。特別是,無需顯式關閉OutputStream,因為OutputStream會在被終結時自行關閉。相反,finalize方法用於釋放由類控制的本機或遠端資源。

類載入

[edit | edit source]

編寫熱部署應用程式的開發人員的主要關注點之一是瞭解類載入的工作原理。在類載入機制的內部,可以找到以下問題的答案:

  • 如果我將實用程式庫的較新版本打包到我的應用程式中,而相同庫的較舊版本仍然存在於伺服器的lib目錄中,會發生什麼?
  • 如何在應用程式伺服器的同一例項中同時使用相同實用程式庫的兩個不同版本?
  • 我目前正在使用哪個版本的實用程式類?
  • 為什麼我需要更改所有這些類載入內容?


Clipboard

待辦事項
新增一些與變數中的練習類似的練習


華夏公益教科書